Files
novalon-website/src/components/content/testimonials.tsx
T
zhangxiang 4b8500af1d fix(theme): 品牌色令牌语义拆分与暗黑模式全站治理
- 拆分品牌色「文字/底色」双通道:新增 --color-brand-ink(-rgb)(深色 #F87171)
  与 tailwind brand.ink;text-brand → text-brand-ink 迁移 247 处;
  bg-brand/border-brand 保持 --color-brand-rgb 不翻转(红底白字 5.58:1 不能动)
- dark 块补齐 --color-brand-bg: #2A1418(修复深色白字压浅粉底 1.04:1)
- hero 徽章新增 --badge-accent-text 桥接:accentColor 直用作深底文字不可读
  (#1e3a5f 深底 1.65:1),浅色取原色 / 深色 color-mix 提亮
- 硬编码颜色 token 化:bg-white/text-gray-*/border-gray-* → bg-bg-*/text-text-*/border-border-*
- 可访问性:触控目标 <24px 归零(footer 链接列 [&_a]:min-h-6 收口 377 处等)、
  news 搜索框补 aria-label、标题跳级 h2→h4 修正为 h3、装饰 blur 光斑容器补
  overflow-hidden 消除伪文本裁剪
- 验证:tsc 0 / eslint 0 error / jest 0 失败用例 / dogfood 33 路由 × 双主题 P1-P3 全零
2026-09-05 08:16:31 +08:00

156 lines
5.6 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { ScrollReveal } from '@/components/ui/scroll-reveal';
import { SectionLabel, EASE_OUT } from '@/components/ui/page-decoration';
import { Quote } from 'lucide-react';
import { useState } from 'react';
import { cn } from '@/lib/utils';
export interface Testimonial {
quote: string;
author: string;
title: string;
company: string;
industry: string;
}
export interface TestimonialSectionProps {
title: string;
subtitle?: string;
testimonials: Testimonial[];
accentColor?: string;
}
export function TestimonialSection({
title,
subtitle,
testimonials,
accentColor = '#C41E3A',
}: TestimonialSectionProps) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<section className="relative py-20 sm:py-28 md:py-32 lg:py-40 overflow-hidden bg-bg-secondary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal className="mb-16 sm:mb-20 max-w-3xl">
<SectionLabel>Client Stories</SectionLabel>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold text-ink tracking-tight leading-tight mb-6">
{title}
</h2>
{subtitle && (
<p className="text-text-secondary leading-relaxed text-lg">
{subtitle}
</p>
)}
</ScrollReveal>
<div className="relative">
<div className="relative overflow-hidden">
<motion.div
className="flex"
animate={{ x: `-${activeIndex * 100}%` }}
transition={{ duration: 0.3, ease: EASE_OUT }}
>
{testimonials.map((testimonial, index) => (
<div key={index} className="w-full shrink-0">
<div className="max-w-4xl mx-auto">
<div className="relative p-10 lg:p-16 border border-border-primary bg-bg-primary">
<Quote className="absolute top-8 left-8 w-12 h-12 opacity-10 text-brand-ink" />
<div className="relative z-10">
<p className="text-xl lg:text-2xl text-text-primary leading-relaxed font-light mb-10 lg:mb-14">
{testimonial.quote}
</p>
<div className="flex items-center gap-5">
<div
className="w-14 h-14 rounded-full flex items-center justify-center text-white font-bold text-lg"
style={{ backgroundColor: accentColor }}
>
{testimonial.author.charAt(0)}
</div>
<div>
<div className="text-text-primary font-semibold text-lg">
{testimonial.author}
</div>
<div className="text-text-secondary text-sm">
{testimonial.title} · {testimonial.company}
</div>
<div className="text-text-muted text-xs mt-0.5">
{testimonial.industry}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
))}
</motion.div>
</div>
<div className="flex justify-center gap-3 mt-10">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => setActiveIndex(index)}
className={cn(
'h-2 rounded-full transition-all duration-300',
activeIndex === index
? 'w-8'
: 'w-2 bg-border-primary hover:bg-border-secondary'
)}
style={activeIndex === index ? { backgroundColor: accentColor } : {}}
/>
))}
</div>
</div>
</div>
</section>
);
}
export interface LogoWallProps {
title: string;
logos: { name: string; industry?: string }[];
}
export function LogoWall({ title, logos }: LogoWallProps) {
return (
<section className="relative py-16 sm:py-20 overflow-hidden bg-bg-primary">
<div className="max-w-container mx-auto px-4 sm:px-6 lg:px-10">
<ScrollReveal>
<div className="text-center mb-12">
<p className="text-sm text-text-muted tracking-[0.2em] uppercase font-medium">
{title}
</p>
</div>
</ScrollReveal>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-px bg-border-primary">
{logos.map((logo, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.3, delay: index * 0.05, ease: EASE_OUT }}
className="flex items-center justify-center p-8 bg-bg-primary hover:bg-bg-secondary transition-colors duration-300"
>
<div className="text-center">
<div className="text-text-secondary font-semibold text-base hover:text-text-primary transition-colors duration-300">
{logo.name}
</div>
{logo.industry && (
<div className="text-text-muted text-xs mt-1">
{logo.industry}
</div>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
}