cleanup
This commit is contained in:
@@ -1,162 +0,0 @@
|
||||
import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
// Badge variants
|
||||
type BadgeVariant = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | 'neutral';
|
||||
|
||||
// Badge sizes
|
||||
type BadgeSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
// Badge props interface
|
||||
interface BadgeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: BadgeVariant;
|
||||
size?: BadgeSize;
|
||||
icon?: ReactNode;
|
||||
iconPosition?: 'left' | 'right';
|
||||
rounded?: boolean;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// Helper function to get variant styles
|
||||
const getVariantStyles = (variant: BadgeVariant) => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return 'bg-primary text-white';
|
||||
case 'secondary':
|
||||
return 'bg-secondary text-white';
|
||||
case 'success':
|
||||
return 'bg-success text-white';
|
||||
case 'warning':
|
||||
return 'bg-warning text-gray-900';
|
||||
case 'error':
|
||||
return 'bg-danger text-white';
|
||||
case 'info':
|
||||
return 'bg-info text-white';
|
||||
case 'neutral':
|
||||
return 'bg-gray-200 text-gray-800';
|
||||
default:
|
||||
return 'bg-primary text-white';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get size styles
|
||||
const getSizeStyles = (size: BadgeSize) => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'text-xs px-2 py-0.5';
|
||||
case 'md':
|
||||
return 'text-sm px-3 py-1';
|
||||
case 'lg':
|
||||
return 'text-base px-4 py-1.5';
|
||||
default:
|
||||
return 'text-sm px-3 py-1';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get icon spacing
|
||||
const getIconSpacing = (size: BadgeSize, iconPosition: 'left' | 'right') => {
|
||||
const spacing = {
|
||||
sm: iconPosition === 'left' ? 'mr-1' : 'ml-1',
|
||||
md: iconPosition === 'left' ? 'mr-1.5' : 'ml-1.5',
|
||||
lg: iconPosition === 'left' ? 'mr-2' : 'ml-2',
|
||||
};
|
||||
return spacing[size];
|
||||
};
|
||||
|
||||
// Helper function to get icon size
|
||||
const getIconSize = (size: BadgeSize) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-3 h-3',
|
||||
md: 'w-4 h-4',
|
||||
lg: 'w-5 h-5',
|
||||
};
|
||||
return sizeClasses[size];
|
||||
};
|
||||
|
||||
// Main Badge Component
|
||||
export const Badge = forwardRef<HTMLDivElement, BadgeProps>(
|
||||
(
|
||||
{
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
icon,
|
||||
iconPosition = 'left',
|
||||
rounded = true,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base styles
|
||||
'inline-flex items-center justify-center font-medium',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
// Variant styles
|
||||
getVariantStyles(variant),
|
||||
// Size styles
|
||||
getSizeStyles(size),
|
||||
// Border radius
|
||||
rounded ? 'rounded-full' : 'rounded-md',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* Icon - Left position */}
|
||||
{icon && iconPosition === 'left' && (
|
||||
<span className={cn('flex items-center justify-center', getIconSpacing(size, 'left'), getIconSize(size))}>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Badge content */}
|
||||
{children && <span>{children}</span>}
|
||||
|
||||
{/* Icon - Right position */}
|
||||
{icon && iconPosition === 'right' && (
|
||||
<span className={cn('flex items-center justify-center', getIconSpacing(size, 'right'), getIconSize(size))}>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Badge.displayName = 'Badge';
|
||||
|
||||
// Badge Group Component for multiple badges
|
||||
interface BadgeGroupProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
gap?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export const BadgeGroup = forwardRef<HTMLDivElement, BadgeGroupProps>(
|
||||
({ gap = 'sm', className = '', children, ...props }, ref) => {
|
||||
const gapClasses = {
|
||||
xs: 'gap-1',
|
||||
sm: 'gap-2',
|
||||
md: 'gap-3',
|
||||
lg: 'gap-4',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('flex flex-wrap items-center', gapClasses[gap], className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
BadgeGroup.displayName = 'BadgeGroup';
|
||||
|
||||
// Export types for external use
|
||||
export type { BadgeProps, BadgeVariant, BadgeSize, BadgeGroupProps };
|
||||
@@ -1,220 +0,0 @@
|
||||
import React, { forwardRef, ButtonHTMLAttributes, ReactNode } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getViewport, getTouchTargetSize } from '../../lib/responsive';
|
||||
|
||||
// Button variants
|
||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost';
|
||||
|
||||
// Button sizes
|
||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||
|
||||
// Button props interface
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: ButtonVariant;
|
||||
size?: ButtonSize;
|
||||
loading?: boolean;
|
||||
icon?: ReactNode;
|
||||
iconPosition?: 'left' | 'right';
|
||||
fullWidth?: boolean;
|
||||
responsiveSize?: {
|
||||
mobile?: ButtonSize;
|
||||
tablet?: ButtonSize;
|
||||
desktop?: ButtonSize;
|
||||
};
|
||||
touchTarget?: boolean;
|
||||
}
|
||||
|
||||
// Helper function to get variant styles
|
||||
const getVariantStyles = (variant: ButtonVariant, disabled?: boolean) => {
|
||||
const baseStyles = 'transition-all duration-200 ease-in-out font-medium rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2';
|
||||
|
||||
if (disabled) {
|
||||
return `${baseStyles} bg-gray-300 text-gray-500 cursor-not-allowed opacity-60`;
|
||||
}
|
||||
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return `${baseStyles} bg-primary hover:bg-primary-dark text-white focus:ring-primary`;
|
||||
case 'secondary':
|
||||
return `${baseStyles} bg-secondary hover:bg-secondary-light text-white focus:ring-secondary`;
|
||||
case 'outline':
|
||||
return `${baseStyles} bg-transparent border-2 border-primary text-primary hover:bg-primary-light hover:border-primary-dark focus:ring-primary`;
|
||||
case 'ghost':
|
||||
return `${baseStyles} bg-transparent text-primary hover:bg-primary-light focus:ring-primary`;
|
||||
default:
|
||||
return `${baseStyles} bg-primary hover:bg-primary-dark text-white`;
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get size styles
|
||||
const getSizeStyles = (size: ButtonSize) => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'px-3 py-1.5 text-sm';
|
||||
case 'md':
|
||||
return 'px-4 py-2 text-base';
|
||||
case 'lg':
|
||||
return 'px-6 py-3 text-lg';
|
||||
default:
|
||||
return 'px-4 py-2 text-base';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get icon spacing
|
||||
const getIconSpacing = (size: ButtonSize, iconPosition: 'left' | 'right') => {
|
||||
const spacing = {
|
||||
sm: iconPosition === 'left' ? 'mr-1.5' : 'ml-1.5',
|
||||
md: iconPosition === 'left' ? 'mr-2' : 'ml-2',
|
||||
lg: iconPosition === 'left' ? 'mr-2.5' : 'ml-2.5',
|
||||
};
|
||||
return spacing[size];
|
||||
};
|
||||
|
||||
// Loading spinner component
|
||||
const LoadingSpinner = ({ size }: { size: ButtonSize }) => {
|
||||
const sizeClasses = {
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('animate-spin', sizeClasses[size])}>
|
||||
<svg
|
||||
className="w-full h-full text-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
className="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
></circle>
|
||||
<path
|
||||
className="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Main Button component
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
variant = 'primary',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
icon,
|
||||
iconPosition = 'left',
|
||||
fullWidth = false,
|
||||
disabled,
|
||||
className = '',
|
||||
children,
|
||||
type = 'button',
|
||||
responsiveSize,
|
||||
touchTarget = true,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const isDisabled = disabled || loading;
|
||||
|
||||
// Get responsive size if provided
|
||||
const getResponsiveSize = () => {
|
||||
if (!responsiveSize) return size;
|
||||
|
||||
if (typeof window === 'undefined') return size;
|
||||
|
||||
const viewport = getViewport();
|
||||
|
||||
if (viewport.isMobile && responsiveSize.mobile) {
|
||||
return responsiveSize.mobile;
|
||||
}
|
||||
if (viewport.isTablet && responsiveSize.tablet) {
|
||||
return responsiveSize.tablet;
|
||||
}
|
||||
if (viewport.isDesktop && responsiveSize.desktop) {
|
||||
return responsiveSize.desktop;
|
||||
}
|
||||
|
||||
return size;
|
||||
};
|
||||
|
||||
const responsiveSizeValue = getResponsiveSize();
|
||||
|
||||
// Get touch target size - fixed for hydration
|
||||
const getTouchTargetClasses = () => {
|
||||
if (!touchTarget) return '';
|
||||
|
||||
// Always return the same classes to avoid hydration mismatch
|
||||
// The touch target is a design requirement that should be consistent
|
||||
return `min-h-[44px] min-w-[44px]`;
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
type={type}
|
||||
disabled={isDisabled}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center font-semibold',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||
// Base styles
|
||||
'rounded-lg',
|
||||
// Variant styles
|
||||
getVariantStyles(variant, isDisabled),
|
||||
// Size styles (responsive)
|
||||
getSizeStyles(responsiveSizeValue),
|
||||
// Touch target optimization
|
||||
getTouchTargetClasses(),
|
||||
// Full width
|
||||
fullWidth ? 'w-full' : '',
|
||||
// Mobile-specific optimizations
|
||||
'active:scale-95 md:active:scale-100',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
// Add aria-label for accessibility if button has only icon
|
||||
aria-label={!children && icon ? 'Button action' : undefined}
|
||||
{...props}
|
||||
>
|
||||
{/* Loading state */}
|
||||
{loading && (
|
||||
<span className={cn('flex items-center justify-center', getIconSpacing(responsiveSizeValue, 'left'))}>
|
||||
<LoadingSpinner size={responsiveSizeValue} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Icon - Left position */}
|
||||
{!loading && icon && iconPosition === 'left' && (
|
||||
<span className={cn('flex items-center justify-center', getIconSpacing(responsiveSizeValue, 'left'))}>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Button content */}
|
||||
{children && <span className="leading-none">{children}</span>}
|
||||
|
||||
{/* Icon - Right position */}
|
||||
{!loading && icon && iconPosition === 'right' && (
|
||||
<span className={cn('flex items-center justify-center', getIconSpacing(responsiveSizeValue, 'right'))}>
|
||||
{icon}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
// Export types for external use
|
||||
export type { ButtonProps, ButtonVariant, ButtonSize };
|
||||
@@ -1,236 +0,0 @@
|
||||
# UI Components Summary
|
||||
|
||||
## ✅ Task Completed Successfully
|
||||
|
||||
All core UI components have been created and are ready for use in the KLZ Cables Next.js application.
|
||||
|
||||
## 📁 Files Created
|
||||
|
||||
### Core Components (6)
|
||||
1. **`Button.tsx`** - Versatile button with variants, sizes, icons, and loading states
|
||||
2. **`Card.tsx`** - Flexible container with header, body, footer, and image support
|
||||
3. **`Container.tsx`** - Responsive wrapper with configurable max-width and padding
|
||||
4. **`Grid.tsx`** - Responsive grid system with 1-12 columns and breakpoints
|
||||
5. **`Badge.tsx`** - Status labels with colors, sizes, and icons
|
||||
6. **`Loading.tsx`** - Spinner animations, loading buttons, and skeleton loaders
|
||||
|
||||
### Supporting Files
|
||||
7. **`index.ts`** - Central export file for all components
|
||||
8. **`ComponentsExample.tsx`** - Comprehensive examples showing all component variations
|
||||
9. **`README.md`** - Complete documentation with usage examples
|
||||
10. **`COMPONENTS_SUMMARY.md`** - This summary file
|
||||
|
||||
### Utility Files
|
||||
11. **`lib/utils.ts`** - Utility functions including `cn()` for class merging
|
||||
|
||||
## 🎨 Design System Foundation
|
||||
|
||||
### Colors (from tailwind.config.js)
|
||||
- **Primary**: `#0056b3` (KLZ blue)
|
||||
- **Secondary**: `#003d82` (darker blue)
|
||||
- **Accent**: `#e6f0ff` (light blue)
|
||||
- **Semantic**: Success, Warning, Error, Info
|
||||
- **Neutral**: Grays for backgrounds and text
|
||||
|
||||
### Typography
|
||||
- **Font**: Inter (system-ui fallback)
|
||||
- **Scale**: xs (0.75rem) to 6xl (3.75rem)
|
||||
- **Weights**: 400, 500, 600, 700, 800
|
||||
|
||||
### Spacing
|
||||
- **Scale**: xs (4px) to 4xl (96px)
|
||||
- **Consistent**: Used across all components
|
||||
|
||||
### Breakpoints
|
||||
- **sm**: 640px
|
||||
- **md**: 768px
|
||||
- **lg**: 1024px
|
||||
- **xl**: 1280px
|
||||
- **2xl**: 1400px
|
||||
|
||||
## 📋 Component Features
|
||||
|
||||
### Button Component
|
||||
```tsx
|
||||
// Variants: primary, secondary, outline, ghost
|
||||
// Sizes: sm, md, lg
|
||||
// Features: icons, loading, disabled, fullWidth
|
||||
<Button variant="primary" size="md" icon={<Icon />} loading>
|
||||
Click me
|
||||
</Button>
|
||||
```
|
||||
|
||||
### Card Component
|
||||
```tsx
|
||||
// Variants: elevated, flat, bordered
|
||||
// Padding: none, sm, md, lg, xl
|
||||
// Features: hoverable, image support, composable
|
||||
<Card variant="elevated" padding="lg" hoverable>
|
||||
<CardHeader title="Title" />
|
||||
<CardBody>Content</CardBody>
|
||||
<CardFooter>Actions</CardFooter>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Container Component
|
||||
```tsx
|
||||
// Max-width: xs to 6xl, full
|
||||
// Padding: none, sm, md, lg, xl, 2xl
|
||||
// Features: centered, fluid
|
||||
<Container maxWidth="6xl" padding="lg">
|
||||
<YourContent />
|
||||
</Container>
|
||||
```
|
||||
|
||||
### Grid Component
|
||||
```tsx
|
||||
// Columns: 1-12
|
||||
// Responsive: colsSm, colsMd, colsLg, colsXl
|
||||
// Gaps: none, xs, sm, md, lg, xl, 2xl
|
||||
<Grid cols={1} colsMd={2} colsLg={4} gap="lg">
|
||||
<GridItem colSpan={2}>Wide</GridItem>
|
||||
<GridItem>Normal</GridItem>
|
||||
</Grid>
|
||||
```
|
||||
|
||||
### Badge Component
|
||||
```tsx
|
||||
// Variants: primary, secondary, success, warning, error, info, neutral
|
||||
// Sizes: sm, md, lg
|
||||
// Features: icons, rounded
|
||||
<Badge variant="success" size="md" icon={<CheckIcon />}>
|
||||
Active
|
||||
</Badge>
|
||||
```
|
||||
|
||||
### Loading Component
|
||||
```tsx
|
||||
// Sizes: sm, md, lg, xl
|
||||
// Variants: primary, secondary, neutral, contrast
|
||||
// Features: overlay, fullscreen, text, skeletons
|
||||
<Loading size="md" overlay text="Loading..." />
|
||||
<LoadingButton text="Processing..." />
|
||||
<LoadingSkeleton width="100%" height="1rem" />
|
||||
```
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
### TypeScript Support
|
||||
- ✅ Fully typed components
|
||||
- ✅ Exported prop interfaces
|
||||
- ✅ Type-safe variants and sizes
|
||||
- ✅ IntelliSense support
|
||||
|
||||
### Accessibility
|
||||
- ✅ ARIA attributes where needed
|
||||
- ✅ Keyboard navigation support
|
||||
- ✅ Focus management
|
||||
- ✅ Screen reader friendly
|
||||
- ✅ Color contrast compliance
|
||||
|
||||
### Responsive Design
|
||||
- ✅ Mobile-first approach
|
||||
- ✅ Tailwind responsive prefixes
|
||||
- ✅ Flexible layouts
|
||||
- ✅ Touch-friendly sizes
|
||||
|
||||
### Performance
|
||||
- ✅ Lightweight components
|
||||
- ✅ Tree-shakeable exports
|
||||
- ✅ No inline styles
|
||||
- ✅ Optimized Tailwind classes
|
||||
|
||||
## 🚀 Usage
|
||||
|
||||
### Quick Start
|
||||
```tsx
|
||||
// Import from central index
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Grid,
|
||||
Badge,
|
||||
Loading
|
||||
} from '@/components/ui';
|
||||
|
||||
// Use in your components
|
||||
export default function MyPage() {
|
||||
return (
|
||||
<Container maxWidth="lg" padding="md">
|
||||
<Grid cols={1} colsMd={2} gap="md">
|
||||
<Card variant="elevated" padding="lg">
|
||||
<CardHeader title="Welcome" />
|
||||
<CardBody>
|
||||
<p>Content goes here</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Button variant="primary">Get Started</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Customization
|
||||
All components support the `className` prop:
|
||||
```tsx
|
||||
<Button
|
||||
variant="primary"
|
||||
className="hover:scale-105 transition-transform"
|
||||
>
|
||||
Custom Button
|
||||
</Button>
|
||||
```
|
||||
|
||||
## 📊 Component Statistics
|
||||
|
||||
- **Total Components**: 6 core + 3 sub-components
|
||||
- **Lines of Code**: ~1,500
|
||||
- **TypeScript Interfaces**: 20+
|
||||
- **Exported Types**: 30+
|
||||
- **Examples**: 50+ variations
|
||||
- **Documentation**: Complete
|
||||
|
||||
## 🎯 Design Principles
|
||||
|
||||
1. **Consistency**: All components follow the same patterns
|
||||
2. **Flexibility**: Props allow for extensive customization
|
||||
3. **Accessibility**: Built with WCAG guidelines in mind
|
||||
4. **Performance**: Optimized for production use
|
||||
5. **Developer Experience**: TypeScript-first, well-documented
|
||||
|
||||
## 🔧 Next Steps
|
||||
|
||||
The components are ready to use immediately. You can:
|
||||
|
||||
1. **Start building**: Import and use components in your pages
|
||||
2. **Customize**: Add your own variants or extend existing ones
|
||||
3. **Test**: Run the development server to see examples
|
||||
4. **Expand**: Add more components as needed (modals, forms, etc.)
|
||||
|
||||
## 📖 Documentation
|
||||
|
||||
For detailed usage examples, see:
|
||||
- `README.md` - Complete component documentation
|
||||
- `ComponentsExample.tsx` - Live examples of all components
|
||||
- Individual component files - Inline documentation
|
||||
|
||||
## ✅ Quality Checklist
|
||||
|
||||
- [x] All components created with TypeScript
|
||||
- [x] Proper prop interfaces and types
|
||||
- [x] Accessibility attributes included
|
||||
- [x] Responsive design implemented
|
||||
- [x] Tailwind CSS classes (no inline styles)
|
||||
- [x] className prop support
|
||||
- [x] forwardRef for better ref handling
|
||||
- [x] Comprehensive examples
|
||||
- [x] Complete documentation
|
||||
- [x] Centralized exports
|
||||
|
||||
---
|
||||
|
||||
**Status**: ✅ **COMPLETE** - All components are production-ready!
|
||||
@@ -1,265 +0,0 @@
|
||||
import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
// Card variants
|
||||
type CardVariant = 'elevated' | 'flat' | 'bordered';
|
||||
|
||||
// Card props interface
|
||||
interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
variant?: CardVariant;
|
||||
children?: ReactNode;
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
hoverable?: boolean;
|
||||
shadow?: boolean;
|
||||
}
|
||||
|
||||
// Card header props
|
||||
interface CardHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'title'> {
|
||||
title?: ReactNode;
|
||||
subtitle?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
action?: ReactNode;
|
||||
}
|
||||
|
||||
// Card body props
|
||||
interface CardBodyProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
// Card footer props
|
||||
interface CardFooterProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
}
|
||||
|
||||
// Card image props
|
||||
interface CardImageProps extends HTMLAttributes<HTMLDivElement> {
|
||||
src: string;
|
||||
alt?: string;
|
||||
height?: 'sm' | 'md' | 'lg' | 'xl';
|
||||
position?: 'top' | 'background';
|
||||
}
|
||||
|
||||
// Helper function to get variant styles
|
||||
const getVariantStyles = (variant: CardVariant) => {
|
||||
switch (variant) {
|
||||
case 'elevated':
|
||||
return 'bg-white shadow-lg shadow-gray-200/50 border border-gray-100';
|
||||
case 'flat':
|
||||
return 'bg-white shadow-sm border border-gray-100';
|
||||
case 'bordered':
|
||||
return 'bg-white border-2 border-gray-200';
|
||||
default:
|
||||
return 'bg-white shadow-md border border-gray-100';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get padding styles
|
||||
const getPaddingStyles = (padding: CardProps['padding']) => {
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return '';
|
||||
case 'sm':
|
||||
return 'p-3';
|
||||
case 'md':
|
||||
return 'p-4';
|
||||
case 'lg':
|
||||
return 'p-6';
|
||||
case 'xl':
|
||||
return 'p-8';
|
||||
default:
|
||||
return 'p-4';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get image height
|
||||
const getImageHeight = (height: CardImageProps['height']) => {
|
||||
switch (height) {
|
||||
case 'sm':
|
||||
return 'h-32';
|
||||
case 'md':
|
||||
return 'h-48';
|
||||
case 'lg':
|
||||
return 'h-64';
|
||||
case 'xl':
|
||||
return 'h-80';
|
||||
default:
|
||||
return 'h-48';
|
||||
}
|
||||
};
|
||||
|
||||
// Main Card Component
|
||||
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
||||
(
|
||||
{
|
||||
variant = 'elevated',
|
||||
padding = 'md',
|
||||
hoverable = false,
|
||||
shadow = true,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'rounded-lg',
|
||||
'transition-all duration-200 ease-in-out',
|
||||
// Variant styles
|
||||
getVariantStyles(variant),
|
||||
// Padding
|
||||
getPaddingStyles(padding),
|
||||
// Hover effect
|
||||
hoverable && 'hover:shadow-xl hover:shadow-gray-200/70 hover:-translate-y-1',
|
||||
// Shadow override
|
||||
!shadow && 'shadow-none',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Card.displayName = 'Card';
|
||||
|
||||
// Card Header Component
|
||||
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
||||
({ title, subtitle, icon, action, className = '', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-start justify-between gap-4',
|
||||
'border-b border-gray-100 pb-4 mb-4',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
{icon && <div className="text-gray-500 mt-0.5">{icon}</div>}
|
||||
<div className="flex-1">
|
||||
{title && (
|
||||
<div className="text-lg font-semibold text-gray-900 leading-tight">
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
{subtitle && (
|
||||
<div className="text-sm text-gray-600 mt-1 leading-relaxed">
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{action && <div className="flex-shrink-0">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardHeader.displayName = 'CardHeader';
|
||||
|
||||
// Card Body Component
|
||||
export const CardBody = forwardRef<HTMLDivElement, CardBodyProps>(
|
||||
({ className = '', children, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn('space-y-3', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardBody.displayName = 'CardBody';
|
||||
|
||||
// Card Footer Component
|
||||
export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
|
||||
({ align = 'left', className = '', children, ...props }, ref) => {
|
||||
const alignmentClasses = {
|
||||
left: 'justify-start',
|
||||
center: 'justify-center',
|
||||
right: 'justify-end',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex items-center gap-3',
|
||||
'border-t border-gray-100 pt-4 mt-4',
|
||||
alignmentClasses[align],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardFooter.displayName = 'CardFooter';
|
||||
|
||||
// Card Image Component
|
||||
export const CardImage = forwardRef<HTMLDivElement, CardImageProps>(
|
||||
({ src, alt, height = 'md', position = 'top', className = '', ...props }, ref) => {
|
||||
const heightClasses = getImageHeight(height);
|
||||
|
||||
if (position === 'background') {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'relative w-full overflow-hidden rounded-t-lg',
|
||||
heightClasses,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'w-full overflow-hidden rounded-t-lg',
|
||||
heightClasses,
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={src}
|
||||
alt={alt || ''}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
CardImage.displayName = 'CardImage';
|
||||
|
||||
// Export types for external use
|
||||
export type { CardProps, CardHeaderProps, CardBodyProps, CardFooterProps, CardImageProps, CardVariant };
|
||||
@@ -1,431 +0,0 @@
|
||||
/**
|
||||
* UI Components Example
|
||||
*
|
||||
* This file demonstrates how to use all the UI components in the design system.
|
||||
* Each component is shown with various props and configurations.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardImage,
|
||||
Container,
|
||||
Grid,
|
||||
GridItem,
|
||||
Badge,
|
||||
BadgeGroup,
|
||||
Loading,
|
||||
LoadingButton,
|
||||
LoadingSkeleton,
|
||||
} from './index';
|
||||
|
||||
// Example Icons (using simple SVG)
|
||||
const ArrowRightIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const StarIcon = () => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
// Button Examples
|
||||
export const ButtonExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Buttons</h3>
|
||||
|
||||
{/* Primary Buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="primary" size="sm">Small Primary</Button>
|
||||
<Button variant="primary" size="md">Medium Primary</Button>
|
||||
<Button variant="primary" size="lg">Large Primary</Button>
|
||||
<Button variant="primary" icon={<ArrowRightIcon />} iconPosition="right">With Icon</Button>
|
||||
<Button variant="primary" loading>Loading</Button>
|
||||
<Button variant="primary" disabled>Disabled</Button>
|
||||
</div>
|
||||
|
||||
{/* Secondary Buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="secondary" size="md">Secondary</Button>
|
||||
<Button variant="secondary" icon={<CheckIcon />}>Success</Button>
|
||||
</div>
|
||||
|
||||
{/* Outline Buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="outline" size="md">Outline</Button>
|
||||
<Button variant="outline" icon={<StarIcon />}>With Icon</Button>
|
||||
</div>
|
||||
|
||||
{/* Ghost Buttons */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Button variant="ghost" size="md">Ghost</Button>
|
||||
<Button variant="ghost" fullWidth>Full Width Ghost</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Card Examples
|
||||
export const CardExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Cards</h3>
|
||||
|
||||
<Grid cols={1} colsMd={2} gap="md">
|
||||
{/* Basic Card */}
|
||||
<Card variant="elevated" padding="lg">
|
||||
<CardHeader
|
||||
title="Basic Card"
|
||||
subtitle="With header and content"
|
||||
/>
|
||||
<CardBody>
|
||||
<p>This is a basic card with elevated variant. It includes a header, body content, and footer.</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Button variant="primary" size="sm">Action</Button>
|
||||
<Button variant="ghost" size="sm">Cancel</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Card with Image */}
|
||||
<Card variant="flat" padding="none">
|
||||
<CardImage
|
||||
src="https://via.placeholder.com/400x200/0056b3/ffffff?text=Card+Image"
|
||||
alt="Card image"
|
||||
height="md"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<CardHeader
|
||||
title="Card with Image"
|
||||
subtitle="Image at the top"
|
||||
/>
|
||||
<CardBody>
|
||||
<p>Cards can include images for visual appeal.</p>
|
||||
</CardBody>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Bordered Card */}
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader
|
||||
title="Bordered Card"
|
||||
icon={<StarIcon />}
|
||||
action={<Badge variant="success">New</Badge>}
|
||||
/>
|
||||
<CardBody>
|
||||
<p>This card has a strong border and includes an icon in the header.</p>
|
||||
</CardBody>
|
||||
<CardFooter align="right">
|
||||
<Button variant="outline" size="sm">Learn More</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Hoverable Card */}
|
||||
<Card variant="elevated" padding="lg" hoverable>
|
||||
<CardHeader title="Hoverable Card" />
|
||||
<CardBody>
|
||||
<p>Hover over this card to see the effect!</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<BadgeGroup gap="sm">
|
||||
<Badge variant="primary">React</Badge>
|
||||
<Badge variant="secondary">TypeScript</Badge>
|
||||
<Badge variant="info">Tailwind</Badge>
|
||||
</BadgeGroup>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Container Examples
|
||||
export const ContainerExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Containers</h3>
|
||||
|
||||
<Container maxWidth="lg" padding="lg" className="bg-gray-50 rounded-lg">
|
||||
<p className="text-center">Default Container (max-width: lg, padding: lg)</p>
|
||||
</Container>
|
||||
|
||||
<Container maxWidth="md" padding="md" className="bg-accent rounded-lg">
|
||||
<p className="text-center">Medium Container (max-width: md, padding: md)</p>
|
||||
</Container>
|
||||
|
||||
<Container fluid className="bg-primary text-white rounded-lg py-4">
|
||||
<p className="text-center">Fluid Container (full width)</p>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Grid Examples
|
||||
export const GridExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Grid System</h3>
|
||||
|
||||
{/* Basic Grid */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600">12-column responsive grid:</p>
|
||||
<Grid cols={2} colsMd={4} gap="sm">
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((item) => (
|
||||
<div key={item} className="bg-primary text-white p-4 rounded text-center">
|
||||
{item}
|
||||
</div>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Grid with Span */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600">Grid with column spans:</p>
|
||||
<Grid cols={3} gap="md">
|
||||
<GridItem colSpan={2} className="bg-secondary text-white p-4 rounded">
|
||||
Span 2 columns
|
||||
</GridItem>
|
||||
<GridItem className="bg-accent p-4 rounded">1 column</GridItem>
|
||||
<GridItem className="bg-warning p-4 rounded">1 column</GridItem>
|
||||
<GridItem colSpan={2} className="bg-success text-white p-4 rounded">
|
||||
Span 2 columns
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Responsive Grid */}
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-gray-600">Responsive (1 col mobile, 2 tablet, 3 desktop):</p>
|
||||
<Grid cols={1} colsSm={2} colsLg={3} gap="lg">
|
||||
{[1, 2, 3, 4, 5, 6].map((item) => (
|
||||
<div key={item} className="bg-gray-200 p-6 rounded text-center font-medium">
|
||||
Item {item}
|
||||
</div>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Badge Examples
|
||||
export const BadgeExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Badges</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{/* Badge Variants */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Color Variants:</p>
|
||||
<BadgeGroup gap="sm">
|
||||
<Badge variant="primary">Primary</Badge>
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="success">Success</Badge>
|
||||
<Badge variant="warning">Warning</Badge>
|
||||
<Badge variant="error">Error</Badge>
|
||||
<Badge variant="info">Info</Badge>
|
||||
<Badge variant="neutral">Neutral</Badge>
|
||||
</BadgeGroup>
|
||||
</div>
|
||||
|
||||
{/* Badge Sizes */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Sizes:</p>
|
||||
<BadgeGroup gap="md">
|
||||
<Badge variant="primary" size="sm">Small</Badge>
|
||||
<Badge variant="primary" size="md">Medium</Badge>
|
||||
<Badge variant="primary" size="lg">Large</Badge>
|
||||
</BadgeGroup>
|
||||
</div>
|
||||
|
||||
{/* Badges with Icons */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">With Icons:</p>
|
||||
<BadgeGroup gap="sm">
|
||||
<Badge variant="success" icon={<CheckIcon />}>Success</Badge>
|
||||
<Badge variant="primary" icon={<StarIcon />} iconPosition="right">Star</Badge>
|
||||
<Badge variant="warning" icon={<ArrowRightIcon />}>Next</Badge>
|
||||
</BadgeGroup>
|
||||
</div>
|
||||
|
||||
{/* Rounded Badges */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Rounded:</p>
|
||||
<BadgeGroup gap="sm">
|
||||
<Badge variant="primary" rounded={true}>Rounded</Badge>
|
||||
<Badge variant="secondary" rounded={false}>Squared</Badge>
|
||||
</BadgeGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Loading Examples
|
||||
export const LoadingExamples = () => (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-semibold mb-2">Loading Components</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Spinner Sizes */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Spinner Sizes:</p>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<Loading size="sm" />
|
||||
<Loading size="md" />
|
||||
<Loading size="lg" />
|
||||
<Loading size="xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Spinner Variants */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Spinner Variants:</p>
|
||||
<div className="flex gap-4 items-center flex-wrap">
|
||||
<Loading size="md" variant="primary" />
|
||||
<Loading size="md" variant="secondary" />
|
||||
<Loading size="md" variant="neutral" />
|
||||
<Loading size="md" variant="contrast" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loading with Text */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">With Text:</p>
|
||||
<Loading size="md" text="Loading data..." />
|
||||
</div>
|
||||
|
||||
{/* Loading Button */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Loading Button:</p>
|
||||
<LoadingButton size="md" text="Processing..." />
|
||||
</div>
|
||||
|
||||
{/* Loading Skeleton */}
|
||||
<div>
|
||||
<p className="text-sm text-gray-600 mb-2">Skeleton Loaders:</p>
|
||||
<div className="space-y-2">
|
||||
<LoadingSkeleton width="100%" height="1rem" />
|
||||
<LoadingSkeleton width="80%" height="1rem" />
|
||||
<LoadingSkeleton width="60%" height="1rem" rounded />
|
||||
<LoadingSkeleton width="100%" height="4rem" rounded />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Combined Example - Full Page Layout
|
||||
export const FullPageExample = () => (
|
||||
<Container maxWidth="6xl" padding="lg">
|
||||
<div className="space-y-8">
|
||||
{/* Header Section */}
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-4xl font-bold text-gray-900">UI Components Showcase</h1>
|
||||
<p className="text-lg text-gray-600">
|
||||
A comprehensive design system for your Next.js application
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Hero Card */}
|
||||
<Card variant="elevated" padding="xl">
|
||||
<CardImage
|
||||
src="https://via.placeholder.com/1200x400/0056b3/ffffff?text=Hero+Image"
|
||||
alt="Hero"
|
||||
height="lg"
|
||||
/>
|
||||
<CardHeader
|
||||
title="Welcome to the Design System"
|
||||
subtitle="Built with accessibility and responsiveness in mind"
|
||||
icon={<StarIcon />}
|
||||
/>
|
||||
<CardBody>
|
||||
<p>
|
||||
This design system provides a complete set of reusable components
|
||||
that follow modern design principles and accessibility standards.
|
||||
</p>
|
||||
</CardBody>
|
||||
<CardFooter align="center">
|
||||
<Button variant="primary" size="lg" icon={<ArrowRightIcon />} iconPosition="right">
|
||||
Get Started
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">Learn More</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
{/* Feature Grid */}
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold mb-4 text-center">Features</h2>
|
||||
<Grid cols={1} colsMd={2} colsLg={3} gap="lg">
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader title="Responsive" icon={<StarIcon />} />
|
||||
<CardBody>
|
||||
<p>Works perfectly on all devices from mobile to desktop.</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Badge variant="success">Ready</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader title="Accessible" icon={<CheckIcon />} />
|
||||
<CardBody>
|
||||
<p>Follows WCAG guidelines with proper ARIA attributes.</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Badge variant="info">WCAG 2.1</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card variant="bordered" padding="md">
|
||||
<CardHeader title="TypeScript" icon={<ArrowRightIcon />} />
|
||||
<CardBody>
|
||||
<p>Fully typed with TypeScript for better developer experience.</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Badge variant="primary">TypeScript</Badge>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Grid>
|
||||
</div>
|
||||
|
||||
{/* Action Section */}
|
||||
<div className="text-center space-y-3">
|
||||
<p className="text-gray-700">Ready to start building?</p>
|
||||
<div className="flex gap-2 justify-center flex-wrap">
|
||||
<Button variant="primary" size="lg">Start Building</Button>
|
||||
<Button variant="secondary" size="lg">View Documentation</Button>
|
||||
<Button variant="ghost" size="lg" icon={<StarIcon />}>Star on GitHub</Button>
|
||||
</div>
|
||||
<BadgeGroup gap="sm" className="justify-center">
|
||||
<Badge variant="primary">React</Badge>
|
||||
<Badge variant="secondary">Next.js</Badge>
|
||||
<Badge variant="info">TypeScript</Badge>
|
||||
<Badge variant="success">Tailwind CSS</Badge>
|
||||
</BadgeGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
||||
// Main Export - All Components
|
||||
export const AllComponentsExample = () => {
|
||||
return (
|
||||
<div className="space-y-12 py-8">
|
||||
<ButtonExamples />
|
||||
<CardExamples />
|
||||
<ContainerExamples />
|
||||
<GridExamples />
|
||||
<BadgeExamples />
|
||||
<LoadingExamples />
|
||||
<FullPageExample />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AllComponentsExample;
|
||||
@@ -1,140 +0,0 @@
|
||||
import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getViewport } from '../../lib/responsive';
|
||||
|
||||
// Container props interface
|
||||
interface ContainerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
maxWidth?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | '4xl' | '5xl' | '6xl' | 'full';
|
||||
padding?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'responsive';
|
||||
centered?: boolean;
|
||||
fluid?: boolean;
|
||||
safeArea?: boolean;
|
||||
responsivePadding?: boolean;
|
||||
}
|
||||
|
||||
// Helper function to get max-width styles
|
||||
const getMaxWidthStyles = (maxWidth: ContainerProps['maxWidth']) => {
|
||||
switch (maxWidth) {
|
||||
case 'xs':
|
||||
return 'max-w-xs';
|
||||
case 'sm':
|
||||
return 'max-w-sm';
|
||||
case 'md':
|
||||
return 'max-w-md';
|
||||
case 'lg':
|
||||
return 'max-w-lg';
|
||||
case 'xl':
|
||||
return 'max-w-xl';
|
||||
case '2xl':
|
||||
return 'max-w-2xl';
|
||||
case '3xl':
|
||||
return 'max-w-3xl';
|
||||
case '4xl':
|
||||
return 'max-w-4xl';
|
||||
case '5xl':
|
||||
return 'max-w-5xl';
|
||||
case '6xl':
|
||||
return 'max-w-6xl';
|
||||
case 'full':
|
||||
return 'max-w-full';
|
||||
default:
|
||||
return 'max-w-6xl';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get padding styles
|
||||
const getPaddingStyles = (padding: ContainerProps['padding'], responsivePadding?: boolean) => {
|
||||
if (padding === 'responsive' || responsivePadding) {
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8 lg:px-10 xl:px-12 2xl:px-16';
|
||||
}
|
||||
|
||||
switch (padding) {
|
||||
case 'none':
|
||||
return 'px-0';
|
||||
case 'sm':
|
||||
return 'px-3 xs:px-4 sm:px-5';
|
||||
case 'md':
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8';
|
||||
case 'lg':
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8 lg:px-10';
|
||||
case 'xl':
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8 lg:px-10 xl:px-12';
|
||||
case '2xl':
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8 lg:px-10 xl:px-12 2xl:px-16';
|
||||
default:
|
||||
return 'px-4 xs:px-5 sm:px-6 md:px-8 lg:px-10';
|
||||
}
|
||||
};
|
||||
|
||||
// Main Container Component
|
||||
export const Container = forwardRef<HTMLDivElement, ContainerProps>(
|
||||
(
|
||||
{
|
||||
maxWidth = '6xl',
|
||||
padding = 'md',
|
||||
centered = true,
|
||||
fluid = false,
|
||||
safeArea = false,
|
||||
responsivePadding = false,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// Get responsive padding if needed
|
||||
const getResponsivePadding = () => {
|
||||
if (!responsivePadding && padding !== 'responsive') return getPaddingStyles(padding, false);
|
||||
|
||||
if (typeof window === 'undefined') return getPaddingStyles('md', true);
|
||||
|
||||
const viewport = getViewport();
|
||||
|
||||
// Mobile-first responsive padding
|
||||
if (viewport.isMobile) {
|
||||
return 'px-4 xs:px-5 sm:px-6';
|
||||
}
|
||||
if (viewport.isTablet) {
|
||||
return 'px-5 sm:px-6 md:px-8 lg:px-10';
|
||||
}
|
||||
if (viewport.isDesktop) {
|
||||
return 'px-6 md:px-8 lg:px-10 xl:px-12';
|
||||
}
|
||||
|
||||
return 'px-6 md:px-8 lg:px-10 xl:px-12 2xl:px-16';
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base container styles
|
||||
'w-full',
|
||||
// Centering
|
||||
centered && 'mx-auto',
|
||||
// Max width
|
||||
!fluid && getMaxWidthStyles(maxWidth),
|
||||
// Padding (responsive or static)
|
||||
responsivePadding || padding === 'responsive' ? getResponsivePadding() : getPaddingStyles(padding, false),
|
||||
// Safe area for mobile notch
|
||||
safeArea && 'safe-area-p',
|
||||
// Mobile-optimized max width
|
||||
'mobile:max-w-full',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
// Add role for accessibility
|
||||
role="region"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Container.displayName = 'Container';
|
||||
|
||||
// Export types for external use
|
||||
export type { ContainerProps };
|
||||
@@ -1,251 +0,0 @@
|
||||
import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { getViewport } from '../../lib/responsive';
|
||||
|
||||
// Grid column types
|
||||
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
|
||||
// Grid gap types
|
||||
type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'responsive';
|
||||
|
||||
// Grid props interface
|
||||
interface GridProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
cols?: GridCols;
|
||||
gap?: GridGap;
|
||||
colsSm?: GridCols;
|
||||
colsMd?: GridCols;
|
||||
colsLg?: GridCols;
|
||||
colsXl?: GridCols;
|
||||
alignItems?: 'start' | 'center' | 'end' | 'stretch';
|
||||
justifyItems?: 'start' | 'center' | 'end' | 'stretch';
|
||||
// Mobile-first stacking
|
||||
stackMobile?: boolean;
|
||||
// Responsive columns
|
||||
responsiveCols?: {
|
||||
mobile?: GridCols;
|
||||
tablet?: GridCols;
|
||||
desktop?: GridCols;
|
||||
};
|
||||
}
|
||||
|
||||
// Grid item props interface
|
||||
interface GridItemProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
colSpan?: GridCols;
|
||||
colSpanSm?: GridCols;
|
||||
colSpanMd?: GridCols;
|
||||
colSpanLg?: GridCols;
|
||||
colSpanXl?: GridCols;
|
||||
rowSpan?: GridCols;
|
||||
rowSpanSm?: GridCols;
|
||||
rowSpanMd?: GridCols;
|
||||
rowSpanLg?: GridCols;
|
||||
rowSpanXl?: GridCols;
|
||||
}
|
||||
|
||||
// Helper function to get gap styles
|
||||
const getGapStyles = (gap: GridGap, responsiveGap?: boolean) => {
|
||||
if (gap === 'responsive' || responsiveGap) {
|
||||
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
}
|
||||
|
||||
switch (gap) {
|
||||
case 'none':
|
||||
return 'gap-0';
|
||||
case 'xs':
|
||||
return 'gap-1';
|
||||
case 'sm':
|
||||
return 'gap-2';
|
||||
case 'md':
|
||||
return 'gap-4';
|
||||
case 'lg':
|
||||
return 'gap-6';
|
||||
case 'xl':
|
||||
return 'gap-8';
|
||||
case '2xl':
|
||||
return 'gap-12';
|
||||
default:
|
||||
return 'gap-4';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get column classes
|
||||
const getColClasses = (cols: GridCols | undefined, breakpoint: string = '') => {
|
||||
if (!cols) return '';
|
||||
const prefix = breakpoint ? `${breakpoint}:` : '';
|
||||
return `${prefix}grid-cols-${cols}`;
|
||||
};
|
||||
|
||||
// Helper function to get span classes
|
||||
const getSpanClasses = (span: GridCols | undefined, type: 'col' | 'row', breakpoint: string = '') => {
|
||||
if (!span) return '';
|
||||
const prefix = breakpoint ? `${breakpoint}:` : '';
|
||||
const typePrefix = type === 'col' ? 'col' : 'row';
|
||||
return `${prefix}${typePrefix}-span-${span}`;
|
||||
};
|
||||
|
||||
// Helper function to get responsive column classes
|
||||
const getResponsiveColClasses = (responsiveCols: GridProps['responsiveCols']) => {
|
||||
if (!responsiveCols) return '';
|
||||
|
||||
let classes = '';
|
||||
|
||||
// Mobile (default)
|
||||
if (responsiveCols.mobile) {
|
||||
classes += `grid-cols-${responsiveCols.mobile} `;
|
||||
}
|
||||
|
||||
// Tablet
|
||||
if (responsiveCols.tablet) {
|
||||
classes += `md:grid-cols-${responsiveCols.tablet} `;
|
||||
}
|
||||
|
||||
// Desktop
|
||||
if (responsiveCols.desktop) {
|
||||
classes += `lg:grid-cols-${responsiveCols.desktop} `;
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
// Main Grid Component
|
||||
export const Grid = forwardRef<HTMLDivElement, GridProps>(
|
||||
(
|
||||
{
|
||||
cols = 1,
|
||||
gap = 'md',
|
||||
colsSm,
|
||||
colsMd,
|
||||
colsLg,
|
||||
colsXl,
|
||||
alignItems,
|
||||
justifyItems,
|
||||
className = '',
|
||||
children,
|
||||
stackMobile = false,
|
||||
responsiveCols,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
// Get responsive column configuration
|
||||
const getResponsiveColumns = () => {
|
||||
if (responsiveCols) {
|
||||
return getResponsiveColClasses(responsiveCols);
|
||||
}
|
||||
|
||||
if (stackMobile) {
|
||||
// Mobile-first: 1 column, then scale up
|
||||
return `grid-cols-1 sm:grid-cols-2 ${colsMd ? `md:grid-cols-${colsMd}` : 'md:grid-cols-3'} ${colsLg ? `lg:grid-cols-${colsLg}` : ''}`;
|
||||
}
|
||||
|
||||
// Default responsive behavior
|
||||
let colClasses = `grid-cols-${cols}`;
|
||||
if (colsSm) colClasses += ` sm:grid-cols-${colsSm}`;
|
||||
if (colsMd) colClasses += ` md:grid-cols-${colsMd}`;
|
||||
if (colsLg) colClasses += ` lg:grid-cols-${colsLg}`;
|
||||
if (colsXl) colClasses += ` xl:grid-cols-${colsXl}`;
|
||||
|
||||
return colClasses;
|
||||
};
|
||||
|
||||
// Get responsive gap
|
||||
const getResponsiveGap = () => {
|
||||
if (gap === 'responsive') {
|
||||
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
}
|
||||
|
||||
// Mobile-first gap scaling
|
||||
if (stackMobile) {
|
||||
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
|
||||
}
|
||||
|
||||
return getGapStyles(gap);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Base grid
|
||||
'grid',
|
||||
// Responsive columns
|
||||
getResponsiveColumns(),
|
||||
// Gap (responsive)
|
||||
getResponsiveGap(),
|
||||
// Alignment
|
||||
alignItems && `items-${alignItems}`,
|
||||
justifyItems && `justify-items-${justifyItems}`,
|
||||
// Mobile-specific: ensure full width
|
||||
'w-full',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
// Add role for accessibility
|
||||
role="grid"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Grid.displayName = 'Grid';
|
||||
|
||||
// Grid Item Component
|
||||
export const GridItem = forwardRef<HTMLDivElement, GridItemProps>(
|
||||
(
|
||||
{
|
||||
colSpan,
|
||||
colSpanSm,
|
||||
colSpanMd,
|
||||
colSpanLg,
|
||||
colSpanXl,
|
||||
rowSpan,
|
||||
rowSpanSm,
|
||||
rowSpanMd,
|
||||
rowSpanLg,
|
||||
rowSpanXl,
|
||||
className = '',
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
// Column spans
|
||||
getSpanClasses(colSpan, 'col'),
|
||||
getSpanClasses(colSpanSm, 'col', 'sm'),
|
||||
getSpanClasses(colSpanMd, 'col', 'md'),
|
||||
getSpanClasses(colSpanLg, 'col', 'lg'),
|
||||
getSpanClasses(colSpanXl, 'col', 'xl'),
|
||||
// Row spans
|
||||
getSpanClasses(rowSpan, 'row'),
|
||||
getSpanClasses(rowSpanSm, 'row', 'sm'),
|
||||
getSpanClasses(rowSpanMd, 'row', 'md'),
|
||||
getSpanClasses(rowSpanLg, 'row', 'lg'),
|
||||
getSpanClasses(rowSpanXl, 'row', 'xl'),
|
||||
// Ensure item doesn't overflow
|
||||
'min-w-0',
|
||||
// Custom classes
|
||||
className
|
||||
)}
|
||||
// Add role for accessibility
|
||||
role="gridcell"
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
GridItem.displayName = 'GridItem';
|
||||
|
||||
// Export types for external use
|
||||
export type { GridProps, GridItemProps, GridCols, GridGap };
|
||||
@@ -1,255 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
|
||||
// Supported icon types
|
||||
export type IconName =
|
||||
// Lucide icons (primary)
|
||||
| 'star' | 'check' | 'x' | 'arrow-left' | 'arrow-right' | 'chevron-left' | 'chevron-right'
|
||||
| 'quote' | 'phone' | 'mail' | 'map-pin' | 'clock' | 'calendar' | 'user' | 'users'
|
||||
| 'award' | 'briefcase' | 'building' | 'globe' | 'settings' | 'tool' | 'wrench'
|
||||
| 'shield' | 'lock' | 'key' | 'heart' | 'thumbs-up' | 'message-circle' | 'phone-call'
|
||||
| 'mail-open' | 'map' | 'navigation' | 'home' | 'info' | 'alert-circle' | 'check-circle'
|
||||
| 'x-circle' | 'plus' | 'minus' | 'search' | 'filter' | 'download' | 'upload'
|
||||
| 'share-2' | 'link' | 'external-link' | 'file-text' | 'file' | 'folder'
|
||||
// Font Awesome style aliases (for WP compatibility)
|
||||
| 'fa-star' | 'fa-check' | 'fa-times' | 'fa-arrow-left' | 'fa-arrow-right'
|
||||
| 'fa-quote-left' | 'fa-phone' | 'fa-envelope' | 'fa-map-marker' | 'fa-clock-o'
|
||||
| 'fa-calendar' | 'fa-user' | 'fa-users' | 'fa-trophy' | 'fa-briefcase'
|
||||
| 'fa-building' | 'fa-globe' | 'fa-cog' | 'fa-wrench' | 'fa-shield'
|
||||
| 'fa-lock' | 'fa-key' | 'fa-heart' | 'fa-thumbs-up' | 'fa-comment'
|
||||
| 'fa-phone-square' | 'fa-envelope-open' | 'fa-map' | 'fa-compass'
|
||||
| 'fa-home' | 'fa-info-circle' | 'fa-check-circle' | 'fa-times-circle'
|
||||
| 'fa-plus' | 'fa-minus' | 'fa-search' | 'fa-filter' | 'fa-download'
|
||||
| 'fa-upload' | 'fa-share-alt' | 'fa-link' | 'fa-external-link'
|
||||
| 'fa-file-text' | 'fa-file' | 'fa-folder';
|
||||
|
||||
export interface IconProps {
|
||||
name: IconName;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'muted' | 'current';
|
||||
strokeWidth?: number;
|
||||
onClick?: () => void;
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon Component
|
||||
* Universal icon component supporting Lucide icons and Font Awesome aliases
|
||||
* Maps WPBakery vc_icon patterns to modern React icons
|
||||
*/
|
||||
export const Icon: React.FC<IconProps> = ({
|
||||
name,
|
||||
size = 'md',
|
||||
className = '',
|
||||
color = 'current',
|
||||
strokeWidth = 2,
|
||||
onClick,
|
||||
ariaLabel
|
||||
}) => {
|
||||
// Map size to actual dimensions
|
||||
const sizeMap = {
|
||||
xs: 'w-3 h-3',
|
||||
sm: 'w-4 h-4',
|
||||
md: 'w-5 h-5',
|
||||
lg: 'w-6 h-6',
|
||||
xl: 'w-8 h-8',
|
||||
'2xl': 'w-10 h-10'
|
||||
};
|
||||
|
||||
// Map color to Tailwind classes
|
||||
const colorMap = {
|
||||
primary: 'text-primary',
|
||||
secondary: 'text-secondary',
|
||||
success: 'text-green-600',
|
||||
warning: 'text-yellow-600',
|
||||
error: 'text-red-600',
|
||||
muted: 'text-gray-500',
|
||||
current: 'text-current'
|
||||
};
|
||||
|
||||
// Normalize icon name (remove fa- prefix and map to Lucide)
|
||||
const normalizeIconName = (iconName: string): string => {
|
||||
// Remove fa- prefix if present
|
||||
const cleanName = iconName.replace(/^fa-/, '');
|
||||
|
||||
// Map common Font Awesome names to Lucide
|
||||
const faToLucide: Record<string, string> = {
|
||||
'star': 'star',
|
||||
'check': 'check',
|
||||
'times': 'x',
|
||||
'arrow-left': 'arrow-left',
|
||||
'arrow-right': 'arrow-right',
|
||||
'quote-left': 'quote',
|
||||
'phone': 'phone',
|
||||
'envelope': 'mail',
|
||||
'map-marker': 'map-pin',
|
||||
'clock-o': 'clock',
|
||||
'calendar': 'calendar',
|
||||
'user': 'user',
|
||||
'users': 'users',
|
||||
'trophy': 'award',
|
||||
'briefcase': 'briefcase',
|
||||
'building': 'building',
|
||||
'globe': 'globe',
|
||||
'cog': 'settings',
|
||||
'wrench': 'wrench',
|
||||
'shield': 'shield',
|
||||
'lock': 'lock',
|
||||
'key': 'key',
|
||||
'heart': 'heart',
|
||||
'thumbs-up': 'thumbs-up',
|
||||
'comment': 'message-circle',
|
||||
'phone-square': 'phone',
|
||||
'envelope-open': 'mail-open',
|
||||
'map': 'map',
|
||||
'compass': 'navigation',
|
||||
'home': 'home',
|
||||
'info-circle': 'info',
|
||||
'check-circle': 'check-circle',
|
||||
'times-circle': 'x-circle',
|
||||
'plus': 'plus',
|
||||
'minus': 'minus',
|
||||
'search': 'search',
|
||||
'filter': 'filter',
|
||||
'download': 'download',
|
||||
'upload': 'upload',
|
||||
'share-alt': 'share-2',
|
||||
'link': 'link',
|
||||
'external-link': 'external-link',
|
||||
'file-text': 'file-text',
|
||||
'file': 'file',
|
||||
'folder': 'folder'
|
||||
};
|
||||
|
||||
return faToLucide[cleanName] || cleanName;
|
||||
};
|
||||
|
||||
const iconName = normalizeIconName(name);
|
||||
const IconComponent = (LucideIcons as any)[iconName];
|
||||
|
||||
if (!IconComponent) {
|
||||
console.warn(`Icon "${name}" (normalized: "${iconName}") not found in Lucide icons`);
|
||||
return (
|
||||
<span className={cn(
|
||||
'inline-flex items-center justify-center',
|
||||
sizeMap[size],
|
||||
colorMap[color],
|
||||
'bg-gray-200 rounded',
|
||||
className
|
||||
)}>
|
||||
?
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
className={cn(
|
||||
'inline-block',
|
||||
sizeMap[size],
|
||||
colorMap[color],
|
||||
'transition-transform duration-150',
|
||||
onClick ? 'cursor-pointer hover:scale-110' : '',
|
||||
className
|
||||
)}
|
||||
strokeWidth={strokeWidth}
|
||||
onClick={onClick}
|
||||
role={onClick ? 'button' : 'img'}
|
||||
aria-label={ariaLabel || name}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper component for icon buttons
|
||||
export const IconButton: React.FC<IconProps & { label?: string }> = ({
|
||||
name,
|
||||
size = 'md',
|
||||
className = '',
|
||||
color = 'primary',
|
||||
onClick,
|
||||
label,
|
||||
ariaLabel
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center gap-2',
|
||||
'rounded-lg transition-all duration-200',
|
||||
'hover:bg-primary/10 active:scale-95',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary/50',
|
||||
className
|
||||
)}
|
||||
aria-label={ariaLabel || label || name}
|
||||
>
|
||||
<Icon name={name} size={size} color={color} />
|
||||
{label && <span className="text-sm font-medium">{label}</span>}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to parse WPBakery vc_icon attributes
|
||||
export function parseWpIcon(iconClass: string): IconProps {
|
||||
// Parse classes like "vc_icon fa fa-star" or "vc_icon lucide-star"
|
||||
const parts = iconClass.split(/\s+/);
|
||||
let name: IconName = 'star';
|
||||
let size: IconProps['size'] = 'md';
|
||||
|
||||
// Find icon name
|
||||
const iconPart = parts.find(p => p.includes('fa-') || p.includes('lucide-') || p === 'fa');
|
||||
if (iconPart) {
|
||||
if (iconPart.includes('fa-')) {
|
||||
name = iconPart.replace('fa-', '') as IconName;
|
||||
} else if (iconPart.includes('lucide-')) {
|
||||
name = iconPart.replace('lucide-', '') as IconName;
|
||||
}
|
||||
}
|
||||
|
||||
// Find size
|
||||
if (parts.includes('fa-lg') || parts.includes('text-xl')) size = 'lg';
|
||||
if (parts.includes('fa-2x')) size = 'xl';
|
||||
if (parts.includes('fa-3x')) size = '2xl';
|
||||
if (parts.includes('fa-xs')) size = 'xs';
|
||||
if (parts.includes('fa-sm')) size = 'sm';
|
||||
|
||||
return { name, size };
|
||||
}
|
||||
|
||||
// Icon wrapper for feature lists
|
||||
export const IconFeature: React.FC<{
|
||||
icon: IconName;
|
||||
title: string;
|
||||
description?: string;
|
||||
iconPosition?: 'top' | 'left';
|
||||
className?: string;
|
||||
}> = ({ icon, title, description, iconPosition = 'left', className = '' }) => {
|
||||
const isLeft = iconPosition === 'left';
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex gap-4',
|
||||
isLeft ? 'flex-row items-start' : 'flex-col items-center text-center',
|
||||
className
|
||||
)}>
|
||||
<Icon
|
||||
name={icon}
|
||||
size="xl"
|
||||
color="primary"
|
||||
className={cn(isLeft ? 'mt-1' : '')}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-1">{title}</h3>
|
||||
{description && (
|
||||
<p className="text-gray-600 text-sm leading-relaxed">{description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Icon;
|
||||
@@ -1,224 +0,0 @@
|
||||
import React, { forwardRef, HTMLAttributes } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
// Loading sizes
|
||||
type LoadingSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
// Loading variants
|
||||
type LoadingVariant = 'primary' | 'secondary' | 'neutral' | 'contrast';
|
||||
|
||||
// Loading props interface
|
||||
interface LoadingProps extends HTMLAttributes<HTMLDivElement> {
|
||||
size?: LoadingSize;
|
||||
variant?: LoadingVariant;
|
||||
overlay?: boolean;
|
||||
text?: string;
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
// Helper function to get size styles
|
||||
const getSizeStyles = (size: LoadingSize) => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'w-4 h-4 border-2';
|
||||
case 'md':
|
||||
return 'w-8 h-8 border-4';
|
||||
case 'lg':
|
||||
return 'w-12 h-12 border-4';
|
||||
case 'xl':
|
||||
return 'w-16 h-16 border-4';
|
||||
default:
|
||||
return 'w-8 h-8 border-4';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get variant styles
|
||||
const getVariantStyles = (variant: LoadingVariant) => {
|
||||
switch (variant) {
|
||||
case 'primary':
|
||||
return 'border-primary';
|
||||
case 'secondary':
|
||||
return 'border-secondary';
|
||||
case 'neutral':
|
||||
return 'border-gray-300';
|
||||
case 'contrast':
|
||||
return 'border-white';
|
||||
default:
|
||||
return 'border-primary';
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get text size
|
||||
const getTextSize = (size: LoadingSize) => {
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
return 'text-sm';
|
||||
case 'md':
|
||||
return 'text-base';
|
||||
case 'lg':
|
||||
return 'text-lg';
|
||||
case 'xl':
|
||||
return 'text-xl';
|
||||
default:
|
||||
return 'text-base';
|
||||
}
|
||||
};
|
||||
|
||||
// Main Loading Component
|
||||
export const Loading = forwardRef<HTMLDivElement, LoadingProps>(
|
||||
({
|
||||
size = 'md',
|
||||
variant = 'primary',
|
||||
overlay = false,
|
||||
text,
|
||||
fullscreen = false,
|
||||
className = '',
|
||||
...props
|
||||
}, ref) => {
|
||||
const spinner = (
|
||||
<div
|
||||
className={cn(
|
||||
'animate-spin rounded-full',
|
||||
'border-t-transparent',
|
||||
getSizeStyles(size),
|
||||
getVariantStyles(variant),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (overlay) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 flex items-center justify-center',
|
||||
'bg-black/50 backdrop-blur-sm',
|
||||
fullscreen && 'w-screen h-screen'
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
{spinner}
|
||||
{text && (
|
||||
<span className={cn('text-white font-medium', getTextSize(size))}>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-col items-center justify-center gap-3',
|
||||
fullscreen && 'w-screen h-screen'
|
||||
)}
|
||||
>
|
||||
{spinner}
|
||||
{text && (
|
||||
<span className={cn('text-gray-700 font-medium', getTextSize(size))}>
|
||||
{text}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Loading.displayName = 'Loading';
|
||||
|
||||
// Loading Button Component
|
||||
interface LoadingButtonProps extends HTMLAttributes<HTMLDivElement> {
|
||||
size?: LoadingSize;
|
||||
variant?: LoadingVariant;
|
||||
text?: string;
|
||||
}
|
||||
|
||||
export const LoadingButton = forwardRef<HTMLDivElement, LoadingButtonProps>(
|
||||
({ size = 'md', variant = 'primary', text = 'Loading...', className = '', ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-2 px-4 py-2 rounded-lg',
|
||||
'bg-gray-100 text-gray-700',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'animate-spin rounded-full border-t-transparent',
|
||||
getSizeStyles(size === 'sm' ? 'sm' : 'md'),
|
||||
getVariantStyles(variant)
|
||||
)}
|
||||
/>
|
||||
<span className="font-medium">{text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LoadingButton.displayName = 'LoadingButton';
|
||||
|
||||
// Loading Skeleton Component
|
||||
interface LoadingSkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
||||
width?: string | number;
|
||||
height?: string | number;
|
||||
rounded?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const LoadingSkeleton = forwardRef<HTMLDivElement, LoadingSkeletonProps>(
|
||||
({ width = '100%', height = '1rem', rounded = false, className = '', ...props }, ref) => {
|
||||
// Convert numeric values to Tailwind width classes
|
||||
const getWidthClass = (width: string | number) => {
|
||||
if (typeof width === 'number') {
|
||||
if (width <= 32) return 'w-8';
|
||||
if (width <= 64) return 'w-16';
|
||||
if (width <= 128) return 'w-32';
|
||||
if (width <= 192) return 'w-48';
|
||||
if (width <= 256) return 'w-64';
|
||||
return 'w-full';
|
||||
}
|
||||
return width === '100%' ? 'w-full' : width;
|
||||
};
|
||||
|
||||
// Convert numeric values to Tailwind height classes
|
||||
const getHeightClass = (height: string | number) => {
|
||||
if (typeof height === 'number') {
|
||||
if (height <= 8) return 'h-2';
|
||||
if (height <= 16) return 'h-4';
|
||||
if (height <= 24) return 'h-6';
|
||||
if (height <= 32) return 'h-8';
|
||||
if (height <= 48) return 'h-12';
|
||||
if (height <= 64) return 'h-16';
|
||||
return 'h-auto';
|
||||
}
|
||||
return height === '1rem' ? 'h-4' : height;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'animate-pulse bg-gray-200',
|
||||
rounded && 'rounded-md',
|
||||
getWidthClass(width),
|
||||
getHeightClass(height),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LoadingSkeleton.displayName = 'LoadingSkeleton';
|
||||
|
||||
// Export types for external use
|
||||
export type { LoadingProps, LoadingSize, LoadingVariant, LoadingButtonProps, LoadingSkeletonProps };
|
||||
@@ -1,367 +0,0 @@
|
||||
# UI Components
|
||||
|
||||
A comprehensive design system of reusable UI components for the KLZ Cables Next.js application. Built with TypeScript, Tailwind CSS, and accessibility best practices.
|
||||
|
||||
## Overview
|
||||
|
||||
This component library provides building blocks for creating consistent, responsive, and accessible user interfaces. All components are fully typed with TypeScript and follow modern web standards.
|
||||
|
||||
## Components
|
||||
|
||||
### 1. Button Component
|
||||
|
||||
A versatile button component with multiple variants, sizes, and states.
|
||||
|
||||
**Features:**
|
||||
- Multiple variants: `primary`, `secondary`, `outline`, `ghost`
|
||||
- Three sizes: `sm`, `md`, `lg`
|
||||
- Icon support with left/right positioning
|
||||
- Loading state with spinner
|
||||
- Full width option
|
||||
- Proper accessibility attributes
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Button } from '@/components/ui';
|
||||
|
||||
// Basic usage
|
||||
<Button variant="primary" size="md">Click me</Button>
|
||||
|
||||
// With icon
|
||||
<Button
|
||||
variant="primary"
|
||||
icon={<ArrowRightIcon />}
|
||||
iconPosition="right"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
|
||||
// Loading state
|
||||
<Button variant="primary" loading>Processing...</Button>
|
||||
|
||||
// Disabled
|
||||
<Button variant="primary" disabled>Not available</Button>
|
||||
|
||||
// Full width
|
||||
<Button variant="primary" fullWidth>Submit</Button>
|
||||
```
|
||||
|
||||
### 2. Card Component
|
||||
|
||||
Flexible container component with optional header, body, footer, and image sections.
|
||||
|
||||
**Features:**
|
||||
- Three variants: `elevated`, `flat`, `bordered`
|
||||
- Optional padding: `none`, `sm`, `md`, `lg`, `xl`
|
||||
- Hover effects
|
||||
- Image support (top or background)
|
||||
- Composable sub-components
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Card, CardHeader, CardBody, CardFooter, CardImage } from '@/components/ui';
|
||||
|
||||
// Basic card
|
||||
<Card variant="elevated" padding="md">
|
||||
<CardHeader title="Title" subtitle="Subtitle" />
|
||||
<CardBody>
|
||||
<p>Card content goes here</p>
|
||||
</CardBody>
|
||||
<CardFooter>
|
||||
<Button variant="primary">Action</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
// Card with image
|
||||
<Card variant="flat" padding="none">
|
||||
<CardImage
|
||||
src="/image.jpg"
|
||||
alt="Description"
|
||||
height="md"
|
||||
/>
|
||||
<div className="p-4">
|
||||
<CardHeader title="Image Card" />
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
// Hoverable card
|
||||
<Card variant="elevated" hoverable>
|
||||
{/* content */}
|
||||
</Card>
|
||||
```
|
||||
|
||||
### 3. Container Component
|
||||
|
||||
Responsive wrapper for centering content with configurable max-width and padding.
|
||||
|
||||
**Features:**
|
||||
- Max-width options: `xs` to `6xl`, `full`
|
||||
- Padding options: `none`, `sm`, `md`, `lg`, `xl`, `2xl`
|
||||
- Centering option
|
||||
- Fluid mode (full width)
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Container } from '@/components/ui';
|
||||
|
||||
// Standard container
|
||||
<Container maxWidth="6xl" padding="lg">
|
||||
<YourContent />
|
||||
</Container>
|
||||
|
||||
// Medium container
|
||||
<Container maxWidth="md" padding="md">
|
||||
<YourContent />
|
||||
</Container>
|
||||
|
||||
// Fluid (full width)
|
||||
<Container fluid>
|
||||
<YourContent />
|
||||
</Container>
|
||||
|
||||
// Without centering
|
||||
<Container maxWidth="lg" centered={false}>
|
||||
<YourContent />
|
||||
</Container>
|
||||
```
|
||||
|
||||
### 4. Grid Component
|
||||
|
||||
Responsive grid system with configurable columns and gaps.
|
||||
|
||||
**Features:**
|
||||
- 1-12 column system
|
||||
- Responsive breakpoints: `colsSm`, `colsMd`, `colsLg`, `colsXl`
|
||||
- Gap spacing: `none`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`
|
||||
- Grid item span control
|
||||
- Alignment options
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Grid, GridItem } from '@/components/ui';
|
||||
|
||||
// Basic grid
|
||||
<Grid cols={3} gap="md">
|
||||
<div>Item 1</div>
|
||||
<div>Item 2</div>
|
||||
<div>Item 3</div>
|
||||
</Grid>
|
||||
|
||||
// Responsive grid
|
||||
<Grid cols={1} colsMd={2} colsLg={4} gap="lg">
|
||||
{/* items */}
|
||||
</Grid>
|
||||
|
||||
// Grid with spans
|
||||
<Grid cols={3} gap="md">
|
||||
<GridItem colSpan={2}>Spans 2 columns</GridItem>
|
||||
<GridItem>1 column</GridItem>
|
||||
</GridItem>
|
||||
|
||||
// Grid with alignment
|
||||
<Grid cols={2} gap="md" alignItems="center" justifyItems="center">
|
||||
{/* items */}
|
||||
</Grid>
|
||||
```
|
||||
|
||||
### 5. Badge Component
|
||||
|
||||
Small status or label component with multiple variants.
|
||||
|
||||
**Features:**
|
||||
- Color variants: `primary`, `secondary`, `success`, `warning`, `error`, `info`, `neutral`
|
||||
- Sizes: `sm`, `md`, `lg`
|
||||
- Icon support with positioning
|
||||
- Rounded or squared
|
||||
- Badge group for multiple badges
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Badge, BadgeGroup } from '@/components/ui';
|
||||
|
||||
// Basic badge
|
||||
<Badge variant="success">Active</Badge>
|
||||
|
||||
// With icon
|
||||
<Badge variant="primary" icon={<StarIcon />}>Featured</Badge>
|
||||
|
||||
// Different sizes
|
||||
<Badge variant="warning" size="sm">Small</Badge>
|
||||
<Badge variant="warning" size="md">Medium</Badge>
|
||||
<Badge variant="warning" size="lg">Large</Badge>
|
||||
|
||||
// Badge group
|
||||
<BadgeGroup gap="sm">
|
||||
<Badge variant="primary">React</Badge>
|
||||
<Badge variant="secondary">TypeScript</Badge>
|
||||
<Badge variant="info">Tailwind</Badge>
|
||||
</BadgeGroup>
|
||||
|
||||
// Rounded
|
||||
<Badge variant="primary" rounded={true}>Rounded</Badge>
|
||||
```
|
||||
|
||||
### 6. Loading Component
|
||||
|
||||
Loading indicators including spinners, buttons, and skeletons.
|
||||
|
||||
**Features:**
|
||||
- Spinner sizes: `sm`, `md`, `lg`, `xl`
|
||||
- Spinner variants: `primary`, `secondary`, `neutral`, `contrast`
|
||||
- Optional text
|
||||
- Overlay mode with fullscreen option
|
||||
- Loading button
|
||||
- Skeleton loaders
|
||||
|
||||
**Usage:**
|
||||
```tsx
|
||||
import { Loading, LoadingButton, LoadingSkeleton } from '@/components/ui';
|
||||
|
||||
// Basic spinner
|
||||
<Loading size="md" />
|
||||
|
||||
// With text
|
||||
<Loading size="md" text="Loading data..." />
|
||||
|
||||
// Overlay (blocks UI)
|
||||
<Loading size="lg" overlay text="Please wait..." />
|
||||
|
||||
// Fullscreen overlay
|
||||
<Loading size="xl" overlay fullscreen text="Loading..." />
|
||||
|
||||
// Loading button
|
||||
<LoadingButton size="md" text="Processing..." />
|
||||
|
||||
// Skeleton loaders
|
||||
<LoadingSkeleton width="100%" height="1rem" />
|
||||
<LoadingSkeleton width="80%" height="1rem" rounded />
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
All components are fully typed with TypeScript. Props are exported for external use:
|
||||
|
||||
```tsx
|
||||
import type {
|
||||
ButtonProps,
|
||||
CardProps,
|
||||
ContainerProps,
|
||||
GridProps,
|
||||
BadgeProps,
|
||||
LoadingProps
|
||||
} from '@/components/ui';
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
All components include proper accessibility attributes:
|
||||
- ARIA labels where needed
|
||||
- Keyboard navigation support
|
||||
- Focus management
|
||||
- Screen reader friendly
|
||||
- Color contrast compliance
|
||||
|
||||
## Responsive Design
|
||||
|
||||
Components are built with mobile-first responsive design:
|
||||
- Tailwind responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`)
|
||||
- Flexible layouts
|
||||
- Touch-friendly sizes
|
||||
- Adaptive spacing
|
||||
|
||||
## Customization
|
||||
|
||||
All components support the `className` prop for custom styling:
|
||||
|
||||
```tsx
|
||||
<Button
|
||||
variant="primary"
|
||||
className="custom-class hover:scale-105"
|
||||
>
|
||||
Custom Button
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always use the index export:**
|
||||
```tsx
|
||||
import { Button, Card } from '@/components/ui';
|
||||
```
|
||||
|
||||
2. **Type your props:**
|
||||
```tsx
|
||||
interface MyComponentProps {
|
||||
title: string;
|
||||
variant?: ButtonVariant;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use semantic HTML:**
|
||||
- Buttons for actions
|
||||
- Links for navigation
|
||||
- Proper heading hierarchy
|
||||
|
||||
4. **Test with keyboard:**
|
||||
- Tab through all interactive elements
|
||||
- Enter/Space activates buttons
|
||||
- Escape closes modals
|
||||
|
||||
5. **Test with screen readers:**
|
||||
- Use VoiceOver (macOS) or NVDA (Windows)
|
||||
- Verify ARIA labels are descriptive
|
||||
|
||||
## Performance Tips
|
||||
|
||||
1. **Lazy load heavy components:**
|
||||
```tsx
|
||||
const HeavyComponent = dynamic(() => import('@/components/Heavy'), {
|
||||
loading: () => <Loading size="md" />
|
||||
});
|
||||
```
|
||||
|
||||
2. **Memoize expensive computations:**
|
||||
```tsx
|
||||
const memoizedValue = useMemo(() => computeExpensiveValue(), [deps]);
|
||||
```
|
||||
|
||||
3. **Use proper image optimization:**
|
||||
```tsx
|
||||
<CardImage
|
||||
src="/optimized-image.jpg"
|
||||
alt="Description"
|
||||
// Next.js Image component recommended for production
|
||||
/>
|
||||
```
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome/Edge: Latest 2 versions
|
||||
- Firefox: Latest 2 versions
|
||||
- Safari: Latest 2 versions
|
||||
- Mobile browsers: iOS Safari, Chrome Mobile
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Modal component
|
||||
- [ ] Dropdown component
|
||||
- [ ] Tabs component
|
||||
- [ ] Accordion component
|
||||
- [ ] Tooltip component
|
||||
- [ ] Toast/Notification component
|
||||
- [ ] Form input components
|
||||
- [ ] Table component
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new components:
|
||||
1. Follow the existing component structure
|
||||
2. Use TypeScript interfaces
|
||||
3. Include proper accessibility attributes
|
||||
4. Add to the index export
|
||||
5. Update this documentation
|
||||
6. Test across browsers and devices
|
||||
|
||||
## License
|
||||
|
||||
Internal use only - KLZ Cables
|
||||
@@ -1,255 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { cn } from '../../lib/utils';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
|
||||
export interface Slide {
|
||||
id: string;
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
ctaText?: string;
|
||||
ctaLink?: string;
|
||||
}
|
||||
|
||||
export interface SliderProps {
|
||||
slides: Slide[];
|
||||
autoplay?: boolean;
|
||||
autoplayInterval?: number;
|
||||
showControls?: boolean;
|
||||
showIndicators?: boolean;
|
||||
variant?: 'default' | 'fullscreen' | 'compact';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Slider Component
|
||||
* Responsive carousel for WPBakery nectar_slider/nectar_carousel patterns
|
||||
* Supports autoplay, manual controls, and multiple variants
|
||||
*/
|
||||
export const Slider: React.FC<SliderProps> = ({
|
||||
slides,
|
||||
autoplay = false,
|
||||
autoplayInterval = 5000,
|
||||
showControls = true,
|
||||
showIndicators = true,
|
||||
variant = 'default',
|
||||
className = ''
|
||||
}) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
// Handle autoplay
|
||||
useEffect(() => {
|
||||
if (!autoplay || slides.length <= 1) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
nextSlide();
|
||||
}, autoplayInterval);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [autoplay, autoplayInterval, currentIndex, slides.length]);
|
||||
|
||||
const nextSlide = useCallback(() => {
|
||||
if (isTransitioning || slides.length <= 1) return;
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex((prev) => (prev + 1) % slides.length);
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
}, [slides.length, isTransitioning]);
|
||||
|
||||
const prevSlide = useCallback(() => {
|
||||
if (isTransitioning || slides.length <= 1) return;
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex((prev) => (prev - 1 + slides.length) % slides.length);
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
}, [slides.length, isTransitioning]);
|
||||
|
||||
const goToSlide = useCallback((index: number) => {
|
||||
if (isTransitioning || slides.length <= 1) return;
|
||||
setIsTransitioning(true);
|
||||
setCurrentIndex(index);
|
||||
setTimeout(() => setIsTransitioning(false), 300);
|
||||
}, [slides.length, isTransitioning]);
|
||||
|
||||
// Variant-specific styles
|
||||
const variantStyles = {
|
||||
default: 'rounded-xl overflow-hidden shadow-lg',
|
||||
fullscreen: 'w-full h-full rounded-none',
|
||||
compact: 'rounded-lg overflow-hidden shadow-md'
|
||||
};
|
||||
|
||||
const heightStyles = {
|
||||
default: 'h-96 md:h-[500px]',
|
||||
fullscreen: 'h-screen',
|
||||
compact: 'h-64 md:h-80'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
'relative w-full bg-gray-900',
|
||||
heightStyles[variant],
|
||||
variantStyles[variant],
|
||||
className
|
||||
)}>
|
||||
{/* Slides Container */}
|
||||
<div className="relative w-full h-full overflow-hidden">
|
||||
{slides.map((slide, index) => (
|
||||
<div
|
||||
key={slide.id}
|
||||
className={cn(
|
||||
'absolute inset-0 w-full h-full transition-opacity duration-500',
|
||||
currentIndex === index ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||
)}
|
||||
>
|
||||
{/* Background Image */}
|
||||
{slide.image && (
|
||||
<div className="absolute inset-0">
|
||||
<div
|
||||
className="w-full h-full bg-cover bg-center"
|
||||
style={{ backgroundImage: `url(${slide.image})` }}
|
||||
/>
|
||||
{/* Overlay for better text readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/40 via-black/20 to-black/60" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 h-full flex flex-col items-center justify-center px-4 md:px-8 text-white text-center">
|
||||
<div className="max-w-4xl space-y-4 md:space-y-6">
|
||||
{slide.subtitle && (
|
||||
<p className={cn(
|
||||
'text-sm md:text-base uppercase tracking-wider font-semibold',
|
||||
'text-white/90',
|
||||
variant === 'compact' && 'text-xs'
|
||||
)}>
|
||||
{slide.subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{slide.title && (
|
||||
<h2 className={cn(
|
||||
'text-3xl md:text-5xl font-bold leading-tight',
|
||||
'text-white drop-shadow-lg',
|
||||
variant === 'compact' && 'text-2xl md:text-3xl'
|
||||
)}>
|
||||
{slide.title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{slide.description && (
|
||||
<p className={cn(
|
||||
'text-lg md:text-xl leading-relaxed',
|
||||
'text-white/90 max-w-2xl mx-auto',
|
||||
variant === 'compact' && 'text-base md:text-lg'
|
||||
)}>
|
||||
{slide.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{slide.ctaText && slide.ctaLink && (
|
||||
<a
|
||||
href={slide.ctaLink}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center',
|
||||
'px-6 py-3 md:px-8 md:py-4',
|
||||
'bg-primary hover:bg-primary-dark',
|
||||
'text-white font-semibold rounded-lg',
|
||||
'transition-all duration-200',
|
||||
'hover:scale-105 active:scale-95',
|
||||
'shadow-lg hover:shadow-xl'
|
||||
)}
|
||||
>
|
||||
{slide.ctaText}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Navigation Controls */}
|
||||
{showControls && slides.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className={cn(
|
||||
'absolute left-4 top-1/2 -translate-y-1/2',
|
||||
'z-20 p-2 md:p-3',
|
||||
'bg-white/20 hover:bg-white/30 backdrop-blur-sm',
|
||||
'text-white rounded-full',
|
||||
'transition-all duration-200',
|
||||
'hover:scale-110 active:scale-95',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
||||
)}
|
||||
aria-label="Previous slide"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5 md:w-6 md:h-6" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={nextSlide}
|
||||
className={cn(
|
||||
'absolute right-4 top-1/2 -translate-y-1/2',
|
||||
'z-20 p-2 md:p-3',
|
||||
'bg-white/20 hover:bg-white/30 backdrop-blur-sm',
|
||||
'text-white rounded-full',
|
||||
'transition-all duration-200',
|
||||
'hover:scale-110 active:scale-95',
|
||||
'focus:outline-none focus:ring-2 focus:ring-white/50'
|
||||
)}
|
||||
aria-label="Next slide"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5 md:w-6 md:h-6" />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Indicators */}
|
||||
{showIndicators && slides.length > 1 && (
|
||||
<div className={cn(
|
||||
'absolute bottom-4 left-1/2 -translate-x-1/2',
|
||||
'z-20 flex gap-2',
|
||||
'bg-black/20 backdrop-blur-sm px-3 py-2 rounded-full'
|
||||
)}>
|
||||
{slides.map((_, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => goToSlide(index)}
|
||||
className={cn(
|
||||
'w-2 h-2 md:w-3 md:h-3 rounded-full',
|
||||
'transition-all duration-200',
|
||||
currentIndex === index
|
||||
? 'bg-white scale-125'
|
||||
: 'bg-white/40 hover:bg-white/60 hover:scale-110'
|
||||
)}
|
||||
aria-label={`Go to slide ${index + 1}`}
|
||||
aria-current={currentIndex === index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Slide Counter (optional, for accessibility) */}
|
||||
<div className={cn(
|
||||
'absolute top-4 right-4',
|
||||
'z-20 px-3 py-1',
|
||||
'bg-black/30 backdrop-blur-sm',
|
||||
'text-white text-sm font-medium rounded-full'
|
||||
)}>
|
||||
{currentIndex + 1} / {slides.length}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Helper function to convert WPBakery slider HTML to Slide array
|
||||
export function parseWpSlider(content: string): Slide[] {
|
||||
// This would parse nectar_slider or similar WPBakery slider patterns
|
||||
// For now, returns empty array - can be enhanced based on actual WP content
|
||||
return [];
|
||||
}
|
||||
|
||||
export default Slider;
|
||||
@@ -1,37 +0,0 @@
|
||||
// UI Components Export
|
||||
export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from './Button';
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
CardImage,
|
||||
type CardProps,
|
||||
type CardHeaderProps,
|
||||
type CardBodyProps,
|
||||
type CardFooterProps,
|
||||
type CardImageProps,
|
||||
type CardVariant
|
||||
} from './Card';
|
||||
export { Container, type ContainerProps } from './Container';
|
||||
export { Grid, GridItem, type GridProps, type GridItemProps, type GridCols, type GridGap } from './Grid';
|
||||
export {
|
||||
Badge,
|
||||
BadgeGroup,
|
||||
type BadgeProps,
|
||||
type BadgeVariant,
|
||||
type BadgeSize,
|
||||
type BadgeGroupProps
|
||||
} from './Badge';
|
||||
export {
|
||||
Loading,
|
||||
LoadingButton,
|
||||
LoadingSkeleton,
|
||||
type LoadingProps,
|
||||
type LoadingSize,
|
||||
type LoadingVariant,
|
||||
type LoadingButtonProps,
|
||||
type LoadingSkeletonProps
|
||||
} from './Loading';
|
||||
export { Slider, type Slide, type SliderProps, parseWpSlider } from './Slider';
|
||||
export { Icon, IconButton, IconFeature, parseWpIcon, type IconProps, type IconName } from './Icon';
|
||||
Reference in New Issue
Block a user