Files
triqura-ecd/components/ui/hero-section-2.tsx
colinislit 273c00f9e8 Epic 2: Design System Migration - Teal-first implementatie
 E2.S1: Tailwind config update
- Teal-700 (#0F766E) als PRIMARY brand color (5.47:1 contrast)
- Amber-600→700 gradient voor AI features
- Updated ring color naar teal-700 voor WCAG AA

 E2.S2: Global CSS variables
- --color-brand → teal-700 (was blue-600)
- --color-info → teal-700 (was blue-500)
- --color-input-focus → teal-700 (was blue-500)
- --color-ai toegevoegd → amber-600

 E2.S3: Component color updates
- components/ui/sign-in.tsx: Alle blue → teal
- components/ui/timeline.tsx: Gradient blue → teal
- components/ui/reading-progress.tsx: Progress bar blue → teal
- components/ui/modern-side-bar.tsx: Logo, active states blue → teal

 E2.S4: AIButton component
- Nieuw component: components/ui/ai-button.tsx
- Amber-600→700 gradient voor WCAG compliance
- 3 variants: default, outline, ghost
- Loading state + Sparkles icon
- Fully accessible (WCAG AA focus states)

 E2.S5: Contrast testing
- scripts/test-contrast.ts: Automated WCAG testing
- docs/design/wcag-compliance.md: Compliance documentatie
- Resultaten: 7/11 AA Normal (4.5:1), 11/11 AA Large (3:1) 

WCAG AA Compliance:  PASS
- Teal-700 op wit: 5.47:1 (AA Normal)
- White op teal-700: 5.47:1 (AA Normal)
- White op amber-600: 3.19:1 (AA Large - OK voor buttons)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 17:38:22 +01:00

162 lines
6.4 KiB
TypeScript

import React from 'react';
import { cn } from "@/lib/utils";
import { motion } from 'framer-motion';
// Icon component for contact details
const InfoIcon = ({ type }: { type: 'website' | 'phone' | 'address' }) => {
const icons = {
website: (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5 text-primary">
<circle cx="12" cy="12" r="10"></circle>
<line x1="2" x2="22" y1="12" y2="12"></line>
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"></path>
</svg>
),
phone: (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5 text-primary">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</svg>
),
address: (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="h-5 w-5 text-primary">
<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
),
};
return <div className="mr-2 flex-shrink-0">{icons[type]}</div>;
};
// Prop types for the HeroSection component
interface HeroSectionProps {
logo?: {
url: string;
alt: string;
text?: string;
};
slogan?: string;
title: React.ReactNode;
subtitle: string;
callToAction: {
text: string;
href: string;
};
backgroundImage: string;
contactInfo: {
website: string;
phone: string;
address: string;
};
className?: string;
}
const HeroSection = React.forwardRef<HTMLDivElement, HeroSectionProps>(
({ className, logo, slogan, title, subtitle, callToAction, backgroundImage, contactInfo, ...props }, ref) => {
// Animation variants for the container to orchestrate children animations
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15,
delayChildren: 0.2,
},
},
};
// Animation variants for individual text/UI elements
const itemVariants = {
hidden: { y: 20, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: {
duration: 0.5,
ease: "easeOut" as const,
},
},
};
return (
<motion.section
ref={ref}
className={cn(
"relative flex w-full flex-col overflow-hidden bg-background text-foreground md:flex-row",
className
)}
initial="hidden"
animate="visible"
variants={containerVariants}
{...props}
>
{/* Left Side: Content */}
<div className="flex w-full flex-col justify-between p-8 md:w-1/2 md:p-12 lg:w-3/5 lg:p-16">
{/* Top Section: Logo & Main Content */}
<div>
<motion.header className="mb-12" variants={itemVariants}>
{logo && (
<div className="flex items-center">
<img src={logo.url} alt={logo.alt} className="mr-3 h-8" />
<div>
{logo.text && <p className="text-lg font-bold text-foreground">{logo.text}</p>}
{slogan && <p className="text-xs tracking-wider text-muted-foreground">{slogan}</p>}
</div>
</div>
)}
</motion.header>
<motion.main variants={containerVariants}>
<motion.h1 className="text-4xl font-bold leading-tight text-foreground md:text-5xl" variants={itemVariants}>
{title}
</motion.h1>
<motion.div className="my-6 h-1 w-20 bg-primary" variants={itemVariants}></motion.div>
<motion.p className="mb-8 max-w-md text-base text-muted-foreground" variants={itemVariants}>
{subtitle}
</motion.p>
<motion.a href={callToAction.href} className="text-lg font-bold tracking-widest text-primary transition-colors hover:text-primary/80" variants={itemVariants}>
{callToAction.text}
</motion.a>
</motion.main>
</div>
{/* Bottom Section: Footer Info */}
<motion.footer className="mt-12 w-full" variants={itemVariants}>
<div className="grid grid-cols-1 gap-6 text-xs text-muted-foreground sm:grid-cols-3">
<div className="flex items-center">
<InfoIcon type="website" />
<span>{contactInfo.website}</span>
</div>
<div className="flex items-center">
<InfoIcon type="phone" />
<span>{contactInfo.phone}</span>
</div>
<div className="flex items-center">
<InfoIcon type="address" />
<span>{contactInfo.address}</span>
</div>
</div>
</motion.footer>
</div>
{/* Right Side: Image with Clip Path Animation */}
<motion.div
className="w-full min-h-[300px] bg-cover bg-center md:w-1/2 md:min-h-full lg:w-2/5"
style={{
backgroundImage: `url(${backgroundImage})`,
}}
initial={{ clipPath: 'polygon(100% 0, 100% 0, 100% 100%, 100% 100%)' }}
animate={{ clipPath: 'polygon(25% 0, 100% 0, 100% 100%, 0% 100%)' }}
transition={{ duration: 1.2, ease: "circOut" }}
>
</motion.div>
</motion.section>
);
}
);
HeroSection.displayName = "HeroSection";
export { HeroSection };