Add improved production UI
This commit is contained in:
@@ -13,12 +13,13 @@ export interface ProgressBarProps {
|
||||
value: number;
|
||||
max?: number;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info' | 'pending' | 'inProgress' | 'completed';
|
||||
variant?: 'default' | 'success' | 'warning' | 'danger' | 'info' | 'pending' | 'inProgress' | 'completed' | 'indeterminate';
|
||||
showLabel?: boolean;
|
||||
label?: string;
|
||||
className?: string;
|
||||
animated?: boolean;
|
||||
customColor?: string;
|
||||
glow?: boolean;
|
||||
}
|
||||
|
||||
const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(({
|
||||
@@ -31,9 +32,11 @@ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(({
|
||||
className,
|
||||
animated = false,
|
||||
customColor,
|
||||
glow = false,
|
||||
...props
|
||||
}, ref) => {
|
||||
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
||||
const isIndeterminate = variant === 'indeterminate';
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-2',
|
||||
@@ -61,8 +64,11 @@ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(({
|
||||
// Check if it's a base color variant (has color scales)
|
||||
if (variant in baseColorMap) {
|
||||
const colors = baseColorMap[variant as keyof typeof baseColorMap];
|
||||
// Enhanced gradient with multiple color stops for a more modern look
|
||||
return {
|
||||
background: `linear-gradient(to right, ${colors[400]}, ${colors[500]})`,
|
||||
background: `linear-gradient(90deg, ${colors[300]}, ${colors[400]}, ${colors[500]}, ${colors[600]})`,
|
||||
backgroundSize: '200% 100%',
|
||||
animation: animated ? 'gradient-shift 2s ease infinite' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,13 +82,27 @@ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(({
|
||||
|
||||
// Default fallback
|
||||
return {
|
||||
background: `linear-gradient(to right, ${baseColors.primary[400]}, ${baseColors.primary[500]})`,
|
||||
background: `linear-gradient(90deg, ${baseColors.primary[300]}, ${baseColors.primary[400]}, ${baseColors.primary[500]}, ${baseColors.primary[600]})`,
|
||||
backgroundSize: '200% 100%',
|
||||
animation: animated ? 'gradient-shift 2s ease infinite' : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Determine if we should show animation
|
||||
const shouldAnimate = animated && percentage < 100 && !isIndeterminate;
|
||||
|
||||
return (
|
||||
<div ref={ref} className={clsx('w-full', className)} {...props}>
|
||||
{(showLabel || label) && (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx('w-full', className)}
|
||||
role="progressbar"
|
||||
aria-valuenow={isIndeterminate ? undefined : value}
|
||||
aria-valuemin={isIndeterminate ? undefined : 0}
|
||||
aria-valuemax={isIndeterminate ? undefined : max}
|
||||
aria-label={label || (isIndeterminate ? "Loading" : `${Math.round(percentage)}% complete`)}
|
||||
{...props}
|
||||
>
|
||||
{(showLabel || label) && !isIndeterminate && (
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-sm font-medium text-[var(--text-primary)]">
|
||||
{label || `${value}/${max}`}
|
||||
@@ -95,26 +115,46 @@ const ProgressBar = forwardRef<HTMLDivElement, ProgressBarProps>(({
|
||||
<div
|
||||
className={clsx(
|
||||
'w-full bg-[var(--bg-quaternary)] rounded-full overflow-hidden',
|
||||
sizeClasses[size]
|
||||
sizeClasses[size],
|
||||
{
|
||||
'animate-pulse': isIndeterminate,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'h-full rounded-full transition-all duration-300 ease-out relative overflow-hidden',
|
||||
'h-full rounded-full transition-all duration-500 ease-out relative overflow-hidden',
|
||||
{
|
||||
'animate-pulse': animated && percentage < 100,
|
||||
'animate-progress-indeterminate': isIndeterminate,
|
||||
'shadow-sm': glow && !isIndeterminate,
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
width: `${percentage}%`,
|
||||
...(customColor ? { backgroundColor: customColor } : getVariantStyle(variant))
|
||||
width: isIndeterminate ? '100%' : `${percentage}%`,
|
||||
...(customColor ? { backgroundColor: customColor } : getVariantStyle(variant)),
|
||||
...(glow && !isIndeterminate ? {
|
||||
boxShadow: `0 0 8px ${customColor || (variant in statusColors ? statusColors[variant as keyof typeof statusColors]?.primary : baseColors[variant as keyof typeof baseColors]?.[500] || baseColors.primary[500])}`
|
||||
} : {})
|
||||
}}
|
||||
>
|
||||
{animated && percentage < 100 && (
|
||||
{/* Animated shimmer effect for active progress */}
|
||||
{shouldAnimate && (
|
||||
<div
|
||||
className="absolute inset-0 opacity-20 animate-pulse"
|
||||
className="absolute inset-0 opacity-30"
|
||||
style={{
|
||||
background: 'linear-gradient(to right, transparent, var(--text-inverse), transparent)'
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent)',
|
||||
animation: 'progress-indeterminate 2s ease infinite',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Enhanced indeterminate animation */}
|
||||
{isIndeterminate && (
|
||||
<div
|
||||
className="absolute inset-0 opacity-40"
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent)',
|
||||
animation: 'progress-indeterminate 1.5s ease infinite',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -223,6 +223,7 @@ export const StatusCard: React.FC<StatusCardProps> = ({
|
||||
animated={progress.percentage < 100}
|
||||
customColor={progress.color || statusIndicator.color}
|
||||
className="w-full"
|
||||
glow={progress.percentage > 10 && progress.percentage < 90}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
import { Card } from '../Card';
|
||||
|
||||
export interface TabItem {
|
||||
@@ -8,15 +8,37 @@ export interface TabItem {
|
||||
}
|
||||
|
||||
export interface TabsProps {
|
||||
items: TabItem[];
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
items?: TabItem[];
|
||||
activeTab?: string;
|
||||
onTabChange?: (tabId: string) => void;
|
||||
variant?: 'default' | 'pills' | 'underline';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
fullWidth?: boolean;
|
||||
className?: string;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onValueChange?: (value: string) => void;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
// Context for compound component pattern
|
||||
interface TabsContextType {
|
||||
activeTab: string;
|
||||
onTabChange: (tabId: string) => void;
|
||||
variant: 'default' | 'pills' | 'underline';
|
||||
size: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextType | null>(null);
|
||||
|
||||
const useTabsContext = () => {
|
||||
const context = useContext(TabsContext);
|
||||
if (!context) {
|
||||
throw new Error('Tabs compound components must be used within a Tabs component');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
const Tabs: React.FC<TabsProps> = ({
|
||||
items,
|
||||
activeTab,
|
||||
@@ -24,8 +46,40 @@ const Tabs: React.FC<TabsProps> = ({
|
||||
variant = 'pills',
|
||||
size = 'md',
|
||||
fullWidth = false,
|
||||
className = ''
|
||||
className = '',
|
||||
defaultValue,
|
||||
value,
|
||||
onValueChange,
|
||||
children
|
||||
}) => {
|
||||
// Handle both controlled and uncontrolled state
|
||||
const [internalActiveTab, setInternalActiveTab] = useState(defaultValue || '');
|
||||
const currentActiveTab = value || activeTab || internalActiveTab;
|
||||
const handleTabChange = (tabId: string) => {
|
||||
if (onValueChange) {
|
||||
onValueChange(tabId);
|
||||
} else if (onTabChange) {
|
||||
onTabChange(tabId);
|
||||
} else {
|
||||
setInternalActiveTab(tabId);
|
||||
}
|
||||
};
|
||||
|
||||
// If children are provided, use compound component pattern
|
||||
if (children) {
|
||||
return (
|
||||
<TabsContext.Provider value={{
|
||||
activeTab: currentActiveTab,
|
||||
onTabChange: handleTabChange,
|
||||
variant,
|
||||
size
|
||||
}}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</TabsContext.Provider>
|
||||
);
|
||||
}
|
||||
// Size classes
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-1.5 text-xs',
|
||||
@@ -119,4 +173,113 @@ const Tabs: React.FC<TabsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Compound Components
|
||||
export interface TabsListProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const TabsList: React.FC<TabsListProps> = ({ className = '', children }) => {
|
||||
const { variant } = useTabsContext();
|
||||
|
||||
const baseClasses = variant === 'underline'
|
||||
? 'border-b border-[var(--border-primary)]'
|
||||
: 'p-1 bg-[var(--bg-secondary)] rounded-lg';
|
||||
|
||||
return (
|
||||
<div className={`flex ${baseClasses} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TabsTriggerProps {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const TabsTrigger: React.FC<TabsTriggerProps> = ({
|
||||
value,
|
||||
children,
|
||||
className = '',
|
||||
disabled = false
|
||||
}) => {
|
||||
const { activeTab, onTabChange, variant, size } = useTabsContext();
|
||||
const isActive = activeTab === value;
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-2 py-1.5 text-xs',
|
||||
md: 'px-3 py-2 text-sm',
|
||||
lg: 'px-4 py-3 text-base'
|
||||
};
|
||||
|
||||
const getVariantClasses = () => {
|
||||
if (disabled) {
|
||||
return 'text-[var(--text-tertiary)] cursor-not-allowed opacity-50';
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'pills':
|
||||
return isActive
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-primary)]';
|
||||
|
||||
case 'underline':
|
||||
return isActive
|
||||
? 'text-[var(--color-primary)] border-b-2 border-[var(--color-primary)]'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] border-b-2 border-transparent';
|
||||
|
||||
default:
|
||||
return isActive
|
||||
? 'bg-[var(--color-primary)] text-white'
|
||||
: 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--bg-secondary)]';
|
||||
}
|
||||
};
|
||||
|
||||
const buttonClasses = `
|
||||
font-medium transition-colors
|
||||
${variant === 'pills' ? 'rounded-md' : ''}
|
||||
${sizeClasses[size]}
|
||||
${getVariantClasses()}
|
||||
${className}
|
||||
`.trim();
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => !disabled && onTabChange(value)}
|
||||
disabled={disabled}
|
||||
className={buttonClasses}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export interface TabsContentProps {
|
||||
value: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TabsContent: React.FC<TabsContentProps> = ({
|
||||
value,
|
||||
children,
|
||||
className = ''
|
||||
}) => {
|
||||
const { activeTab } = useTabsContext();
|
||||
|
||||
if (activeTab !== value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`mt-4 ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tabs;
|
||||
@@ -1,2 +1,8 @@
|
||||
export { default as Tabs } from './Tabs';
|
||||
export type { TabsProps, TabItem } from './Tabs';
|
||||
export { default as Tabs, TabsList, TabsTrigger, TabsContent } from './Tabs';
|
||||
export type {
|
||||
TabsProps,
|
||||
TabItem,
|
||||
TabsListProps,
|
||||
TabsTriggerProps,
|
||||
TabsContentProps
|
||||
} from './Tabs';
|
||||
129
frontend/src/components/ui/Textarea/Textarea.tsx
Normal file
129
frontend/src/components/ui/Textarea/Textarea.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import React, { forwardRef, TextareaHTMLAttributes } from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
export interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
helperText?: string;
|
||||
isRequired?: boolean;
|
||||
isInvalid?: boolean;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
variant?: 'outline' | 'filled' | 'unstyled';
|
||||
resize?: 'none' | 'vertical' | 'horizontal' | 'both';
|
||||
}
|
||||
|
||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(({
|
||||
label,
|
||||
error,
|
||||
helperText,
|
||||
isRequired = false,
|
||||
isInvalid = false,
|
||||
size = 'md',
|
||||
variant = 'outline',
|
||||
resize = 'vertical',
|
||||
className,
|
||||
id,
|
||||
rows = 3,
|
||||
...props
|
||||
}, ref) => {
|
||||
const textareaId = id || `textarea-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const hasError = isInvalid || !!error;
|
||||
|
||||
const baseTextareaClasses = [
|
||||
'w-full transition-colors duration-200',
|
||||
'focus:outline-none',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'placeholder:text-[var(--text-tertiary)]'
|
||||
];
|
||||
|
||||
const variantClasses = {
|
||||
outline: [
|
||||
'bg-[var(--bg-primary)] border border-[var(--border-secondary)]',
|
||||
'focus:border-[var(--color-primary)] focus:ring-1 focus:ring-[var(--color-primary)]',
|
||||
hasError ? 'border-[var(--color-error)] focus:border-[var(--color-error)] focus:ring-[var(--color-error)]' : ''
|
||||
],
|
||||
filled: [
|
||||
'bg-[var(--bg-secondary)] border border-transparent',
|
||||
'focus:bg-[var(--bg-primary)] focus:border-[var(--color-primary)]',
|
||||
hasError ? 'border-[var(--color-error)]' : ''
|
||||
],
|
||||
unstyled: [
|
||||
'bg-transparent border-none',
|
||||
'focus:ring-0'
|
||||
]
|
||||
};
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'px-3 py-2 text-sm',
|
||||
md: 'px-4 py-3 text-base',
|
||||
lg: 'px-5 py-4 text-lg'
|
||||
};
|
||||
|
||||
const resizeClasses = {
|
||||
none: 'resize-none',
|
||||
vertical: 'resize-y',
|
||||
horizontal: 'resize-x',
|
||||
both: 'resize'
|
||||
};
|
||||
|
||||
const textareaClasses = clsx(
|
||||
baseTextareaClasses,
|
||||
variantClasses[variant],
|
||||
sizeClasses[size],
|
||||
resizeClasses[resize],
|
||||
'rounded-lg',
|
||||
className
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={textareaId}
|
||||
className="block text-sm font-medium text-[var(--text-primary)] mb-2"
|
||||
>
|
||||
{label}
|
||||
{isRequired && (
|
||||
<span className="text-[var(--color-error)] ml-1">*</span>
|
||||
)}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<textarea
|
||||
ref={ref}
|
||||
id={textareaId}
|
||||
rows={rows}
|
||||
className={textareaClasses}
|
||||
aria-invalid={hasError}
|
||||
aria-describedby={
|
||||
error ? `${textareaId}-error` :
|
||||
helperText ? `${textareaId}-helper` :
|
||||
undefined
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<p
|
||||
id={`${textareaId}-error`}
|
||||
className="mt-2 text-sm text-[var(--color-error)]"
|
||||
>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{helperText && !error && (
|
||||
<p
|
||||
id={`${textareaId}-helper`}
|
||||
className="mt-2 text-sm text-[var(--text-secondary)]"
|
||||
>
|
||||
{helperText}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
export default Textarea;
|
||||
2
frontend/src/components/ui/Textarea/index.ts
Normal file
2
frontend/src/components/ui/Textarea/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as Textarea } from './Textarea';
|
||||
export type { TextareaProps } from './Textarea';
|
||||
@@ -1,6 +1,7 @@
|
||||
// UI Components - Design System
|
||||
export { default as Button } from './Button';
|
||||
export { default as Input } from './Input';
|
||||
export { default as Textarea } from './Textarea/Textarea';
|
||||
export { default as Card, CardHeader, CardBody, CardFooter } from './Card';
|
||||
export { default as Modal, ModalHeader, ModalBody, ModalFooter } from './Modal';
|
||||
export { default as Table } from './Table';
|
||||
@@ -22,6 +23,7 @@ export { TenantSwitcher } from './TenantSwitcher';
|
||||
// Export types
|
||||
export type { ButtonProps } from './Button';
|
||||
export type { InputProps } from './Input';
|
||||
export type { TextareaProps } from './Textarea';
|
||||
export type { CardProps, CardHeaderProps, CardBodyProps, CardFooterProps } from './Card';
|
||||
export type { ModalProps, ModalHeaderProps, ModalBodyProps, ModalFooterProps } from './Modal';
|
||||
export type { TableProps, TableColumn, TableRow } from './Table';
|
||||
|
||||
Reference in New Issue
Block a user