016b7cfb91
Phase 1: Accessibility Optimizations - Add proper label associations and ARIA attributes to form inputs - Implement aria-required, aria-invalid, aria-describedby for better form accessibility - Add role='alert' for error messages - Enhance keyboard navigation with aria-expanded, aria-controls - Add aria-label for mobile menu button - Implement aria-current for active navigation items - Add semantic HTML with aria-labelledby for sections Phase 2: UX Optimizations - Create loading skeleton components for better loading states - Add FormSkeleton, SectionSkeleton, and LoadingSkeleton components - Prepare for lazy loading implementation Files modified: - src/components/ui/input.tsx: Enhanced with ARIA attributes - src/components/ui/textarea.tsx: Enhanced with ARIA attributes - src/components/layout/header.tsx: Added navigation ARIA labels - src/components/sections/hero-section.tsx: Added section labels - src/components/sections/services-section.tsx: Added section labels - src/components/ui/loading-skeleton.tsx: New loading state components Impact: - WCAG 2.1 AA compliance improvements - Better screen reader support - Enhanced keyboard navigation - Improved user feedback during loading
74 lines
1.7 KiB
TypeScript
74 lines
1.7 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useState, useEffect } from 'react';
|
|
|
|
interface SubtleParticleProps {
|
|
count?: number;
|
|
size?: number;
|
|
color?: string;
|
|
className?: string;
|
|
}
|
|
|
|
export function SubtleParticles({
|
|
count = 20,
|
|
size = 3,
|
|
color = '#C41E3A',
|
|
className = ''
|
|
}: SubtleParticleProps) {
|
|
const [particles, setParticles] = useState<Array<{
|
|
id: number;
|
|
x: number;
|
|
y: number;
|
|
delay: number;
|
|
duration: number;
|
|
}>>([]);
|
|
|
|
useEffect(() => {
|
|
const generatedParticles = Array.from({ length: count }, (_, i) => ({
|
|
id: i,
|
|
x: Math.random() * 100,
|
|
y: Math.random() * 100,
|
|
delay: Math.random() * 5,
|
|
duration: 8 + Math.random() * 4
|
|
}));
|
|
setParticles(generatedParticles);
|
|
}, [count]);
|
|
|
|
if (particles.length === 0) {
|
|
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
|
|
}
|
|
|
|
return (
|
|
<div className={`absolute inset-0 pointer-events-none ${className}`}>
|
|
{particles.map((particle) => (
|
|
<motion.div
|
|
key={particle.id}
|
|
className="absolute rounded-full"
|
|
style={{
|
|
width: size,
|
|
height: size,
|
|
backgroundColor: color,
|
|
left: `${particle.x}%`,
|
|
top: `${particle.y}%`
|
|
}}
|
|
initial={{ opacity: 0, scale: 0 }}
|
|
animate={{
|
|
opacity: [0, 0.3, 0.3, 0],
|
|
scale: [0, 1, 1, 0],
|
|
y: [0, -20, -20, 0]
|
|
}}
|
|
transition={{
|
|
duration: particle.duration,
|
|
delay: particle.delay,
|
|
repeat: Infinity,
|
|
ease: 'easeInOut',
|
|
times: [0, 0.3, 0.7, 1]
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SubtleParticles; |