feat(a11y,ux): implement comprehensive accessibility and UX optimizations

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
This commit is contained in:
张翔
2026-02-24 00:40:19 +08:00
parent 44ba75e4d1
commit 016b7cfb91
22 changed files with 2479 additions and 7 deletions
+71
View File
@@ -0,0 +1,71 @@
'use client';
import { motion } from 'framer-motion';
import { useState, useEffect } from 'react';
interface SubtleDotsProps {
className?: string;
color?: string;
count?: number;
}
export function SubtleDots({
className = '',
color = '#C41E3A',
count = 12
}: SubtleDotsProps) {
const [dots, setDots] = useState<Array<{
id: number;
x: number;
y: number;
size: number;
delay: number;
}>>([]);
useEffect(() => {
const generatedDots = Array.from({ length: count }, (_, i) => ({
id: i,
x: 10 + Math.random() * 80,
y: 10 + Math.random() * 80,
size: 2 + Math.random() * 3,
delay: i * 0.3
}));
setDots(generatedDots);
}, [count]);
if (dots.length === 0) {
return <div className={`absolute inset-0 pointer-events-none ${className}`} />;
}
return (
<div className={`absolute inset-0 pointer-events-none ${className}`}>
{dots.map((dot) => (
<motion.div
key={dot.id}
className="absolute rounded-full"
style={{
width: dot.size,
height: dot.size,
backgroundColor: color,
left: `${dot.x}%`,
top: `${dot.y}%`
}}
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0, 0.2, 0.2, 0],
scale: [0, 1, 1, 0]
}}
transition={{
duration: 4,
delay: dot.delay,
repeat: Infinity,
ease: 'easeInOut',
times: [0, 0.3, 0.7, 1]
}}
/>
))}
</div>
);
}
export default SubtleDots;