chore(cleanup): 清理废弃组件与旧版本归档
- 删除 detail-v2 旧版叙事组件 - 删除 narrative 旧版区块组件 - 删除 theme-toggle 主题切换与 theme-context 上下文 - 删除 detail-data-proof/service-value/solution-value 旧版组件 - 删除 home-content-v2/v3 旧版首页 - 归档旧版营销页面内容至 _archive 目录
This commit is contained in:
@@ -1,122 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useState } from 'react';
|
||||
import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ArrowRight, MessageCircle } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
|
||||
export interface NarrativeCTAProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
primaryLabel?: string;
|
||||
primaryHref?: string;
|
||||
secondaryLabel?: string;
|
||||
secondaryHref?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 第四层:行动号召
|
||||
* 全宽深色背景 + 水墨纹理 + 双CTA按钮
|
||||
*/
|
||||
export function NarrativeCTA({
|
||||
title = '一起聊聊您的数字化需求',
|
||||
description = '无论您处于数字化转型的哪个阶段,我们都愿意坐下来一起想办法。首次咨询免费,无任何销售压力。',
|
||||
primaryLabel = '预约免费咨询',
|
||||
primaryHref = '/contact',
|
||||
secondaryLabel = '了解我们的方法',
|
||||
secondaryHref = '/team',
|
||||
}: NarrativeCTAProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseX = useMotionValue(0);
|
||||
const mouseY = useMotionValue(0);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const springX = useSpring(mouseX, { stiffness: 80, damping: 20 });
|
||||
const springY = useSpring(mouseY, { stiffness: 80, damping: 20 });
|
||||
|
||||
const glowX = useTransform(springX, [0, 1], ['-10%', '110%']);
|
||||
const glowY = useTransform(springY, [0, 1], ['-10%', '110%']);
|
||||
|
||||
function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
|
||||
if (!containerRef.current || shouldReduceMotion) return;
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
mouseX.set((e.clientX - rect.left) / rect.width);
|
||||
mouseY.set((e.clientY - rect.top) / rect.height);
|
||||
}
|
||||
|
||||
return (
|
||||
<section
|
||||
id="cta"
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
className="relative py-24 md:py-32 overflow-hidden"
|
||||
style={{ backgroundColor: 'var(--color-cta-bg)' }}
|
||||
>
|
||||
{/* Ink texture background */}
|
||||
<div className="pointer-events-none absolute inset-0">
|
||||
{!shouldReduceMotion && (
|
||||
<motion.div
|
||||
className="absolute inset-[-20%]"
|
||||
style={{
|
||||
background: 'radial-gradient(circle at center, rgba(196, 30, 58, 0.06) 0%, transparent 50%)',
|
||||
x: glowX,
|
||||
y: glowY,
|
||||
opacity: isHovered ? 1 : 0.4,
|
||||
}}
|
||||
transition={{ opacity: { duration: 0.6 } }}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: 'repeat',
|
||||
backgroundSize: '200px 200px',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, scale: 0.97, y: 16 }}
|
||||
whileInView={{ opacity: 1, scale: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-80px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="text-center max-w-3xl mx-auto"
|
||||
>
|
||||
<h2 className="text-3xl sm:text-4xl lg:text-5xl font-semibold text-white mb-6 tracking-tight leading-tight">
|
||||
{title}
|
||||
</h2>
|
||||
<p className="text-lg text-white/60 leading-relaxed mb-10 max-w-2xl mx-auto">
|
||||
{description}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<Button size="lg" asChild className="group relative overflow-hidden">
|
||||
<StaticLink href={primaryHref}>
|
||||
<span className="relative z-10 flex items-center">
|
||||
<MessageCircle className="w-4 h-4 mr-2" />
|
||||
{primaryLabel}
|
||||
<ArrowRight className="w-4 h-4 ml-2 group-hover:translate-x-1 transition-transform" />
|
||||
</span>
|
||||
</StaticLink>
|
||||
</Button>
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="border-white/20 text-white hover:bg-white/10 hover:border-white/35 backdrop-blur-sm"
|
||||
asChild
|
||||
>
|
||||
<StaticLink href={secondaryHref}>{secondaryLabel}</StaticLink>
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { StaticLink } from '@/components/ui/static-link';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface NarrativeHeroProps {
|
||||
/** Page badge/tag */
|
||||
badge?: { text: string; variant?: 'brand' | 'blue' | 'green' | 'purple' };
|
||||
/** Main headline */
|
||||
title: string;
|
||||
/** Keyword highlighted in calligraphy font + brand red */
|
||||
highlight?: string;
|
||||
/** Subtitle / one-line value proposition */
|
||||
subtitle: string;
|
||||
/** Description paragraph */
|
||||
description?: string;
|
||||
/** Primary CTA */
|
||||
primaryCta?: { label: string; href: string };
|
||||
/** Secondary CTA */
|
||||
secondaryCta?: { label: string; href: string };
|
||||
/** Right side content (card, image, etc.) */
|
||||
rightContent?: ReactNode;
|
||||
/** Stats row */
|
||||
stats?: { value: string; label: string }[];
|
||||
/** Extra content below description */
|
||||
extraContent?: ReactNode;
|
||||
}
|
||||
|
||||
const EASE = [0.25, 1, 0.5, 1] as const;
|
||||
|
||||
export function NarrativeHero({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
description,
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
rightContent,
|
||||
stats,
|
||||
extraContent,
|
||||
}: NarrativeHeroProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const fadeUp = (delay: number) => ({
|
||||
initial: shouldReduceMotion ? {} : { opacity: 0, y: 20 },
|
||||
animate: { opacity: 1, y: 0 },
|
||||
transition: { duration: 0.6, delay, ease: EASE },
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
className="relative min-h-[80vh] flex flex-col justify-center overflow-hidden bg-[var(--color-bg-primary)]"
|
||||
>
|
||||
<InkWashBackground variant="hero" />
|
||||
|
||||
<div className="container-wide py-20 md:py-28 lg:py-36 relative z-10 flex-1 flex items-center">
|
||||
<div className={`grid grid-cols-1 ${rightContent ? 'lg:grid-cols-2 gap-10 lg:gap-16' : 'max-w-4xl mx-auto text-center'} items-center w-full`}>
|
||||
<div className={rightContent ? '' : 'mx-auto'}>
|
||||
{/* Badge */}
|
||||
{badge && (
|
||||
<motion.div {...fadeUp(0)} className="mb-6">
|
||||
<span className="inline-flex items-center gap-2 px-3 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] text-[var(--color-brand-primary)] text-xs font-medium border border-[var(--color-brand-primary)]/10">
|
||||
{badge.text}
|
||||
</span>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Subtitle above title */}
|
||||
<motion.p
|
||||
{...fadeUp(0.05)}
|
||||
className="text-base sm:text-lg text-[var(--color-text-muted)] mb-3"
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
|
||||
{/* Title with calligraphy highlight */}
|
||||
<motion.h1
|
||||
{...fadeUp(0.1)}
|
||||
className="text-4xl sm:text-5xl lg:text-6xl xl:text-7xl tracking-tight mb-6 leading-[1.12]"
|
||||
>
|
||||
{highlight ? (
|
||||
<>
|
||||
{title}{' '}
|
||||
<span className="text-[var(--color-brand-primary)] font-calligraphy">{highlight}</span>
|
||||
</>
|
||||
) : (
|
||||
title
|
||||
)}
|
||||
</motion.h1>
|
||||
|
||||
{/* Description */}
|
||||
{description && (
|
||||
<motion.p
|
||||
{...fadeUp(0.2)}
|
||||
className="text-base sm:text-lg text-[var(--color-text-muted)] max-w-lg leading-relaxed mb-8"
|
||||
>
|
||||
{description}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{/* CTAs */}
|
||||
{(primaryCta || secondaryCta) && (
|
||||
<motion.div
|
||||
{...fadeUp(0.3)}
|
||||
className={`flex ${rightContent ? 'flex-col sm:flex-row items-start' : 'flex-col sm:flex-row items-center justify-center'} gap-4 mb-10`}
|
||||
>
|
||||
{primaryCta && (
|
||||
<Button size="lg" asChild className="min-h-[52px] px-8 text-base font-semibold shadow-lg shadow-[var(--color-brand-primary)]/15">
|
||||
<StaticLink href={primaryCta.href}>
|
||||
{primaryCta.label}
|
||||
<ArrowRight className="w-4 h-4 ml-2" />
|
||||
</StaticLink>
|
||||
</Button>
|
||||
)}
|
||||
{secondaryCta && (
|
||||
<Button size="lg" variant="ghost" asChild className="text-[var(--color-text-muted)]">
|
||||
<StaticLink href={secondaryCta.href}>{secondaryCta.label}</StaticLink>
|
||||
</Button>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Extra content */}
|
||||
{extraContent}
|
||||
</div>
|
||||
|
||||
{/* Right side content */}
|
||||
{rightContent && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 28 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.4, ease: EASE }}
|
||||
>
|
||||
{rightContent}
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats row */}
|
||||
{stats && stats.length > 0 && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5, delay: 0.6, ease: EASE }}
|
||||
className="container-wide pb-16 relative z-10"
|
||||
>
|
||||
<div className="ink-divider mb-10" />
|
||||
<div className="flex flex-wrap justify-center gap-8 md:gap-16">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="text-center">
|
||||
<div className="text-3xl md:text-4xl font-bold text-[var(--color-text-primary)] leading-none mb-1">
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--color-text-subtle)] tracking-wide">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export interface NarrativeTrustProps {
|
||||
/** Section badge */
|
||||
badge?: string;
|
||||
/** Section title */
|
||||
title: string;
|
||||
/** Title highlight */
|
||||
highlight?: string;
|
||||
/** Stat metrics */
|
||||
metrics?: { value: string; label: string; description?: string }[];
|
||||
/** Custom content (e.g., case studies, testimonials) */
|
||||
children?: ReactNode;
|
||||
/** Background variant */
|
||||
bgVariant?: 'default' | 'section';
|
||||
}
|
||||
|
||||
/**
|
||||
* 第三层:信任证明
|
||||
* 数据指标 + 案例摘要 + 资质认证
|
||||
*/
|
||||
export function NarrativeTrust({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
metrics,
|
||||
children,
|
||||
bgVariant = 'default',
|
||||
}: NarrativeTrustProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
return (
|
||||
<section className={`relative section-padding overflow-hidden ${bgVariant === 'section' ? 'bg-[var(--color-bg-section)]' : 'bg-[var(--color-bg-primary)]'}`}>
|
||||
<InkWashBackground variant="subtle" />
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="mb-14"
|
||||
>
|
||||
{badge && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] border border-[var(--color-brand-primary)]/10 mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<span className="text-xs font-medium tracking-wider text-[var(--color-brand-primary)]">{badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
{title}
|
||||
{highlight && <span className="text-[var(--color-brand-primary)] font-calligraphy ml-1">{highlight}</span>}
|
||||
</h2>
|
||||
</motion.div>
|
||||
|
||||
{/* Metrics strip */}
|
||||
{metrics && metrics.length > 0 && (
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.5, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-8 mb-16"
|
||||
>
|
||||
{metrics.map((metric, idx) => (
|
||||
<motion.div
|
||||
key={metric.label}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.4, delay: idx * 0.06 }}
|
||||
className="text-center p-6 rounded-xl bg-[var(--color-bg-primary)] border border-[var(--color-border-primary)]"
|
||||
>
|
||||
<div className="text-3xl md:text-4xl font-bold text-[var(--color-brand-primary)] leading-none mb-2">
|
||||
{metric.value}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-[var(--color-text-primary)] mb-1">{metric.label}</div>
|
||||
{metric.description && (
|
||||
<div className="text-xs text-[var(--color-text-subtle)]">{metric.description}</div>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Custom content */}
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
import { InkCard } from '@/components/ui/ink-wash';
|
||||
import { InkWashBackground } from '@/components/ui/ink-wash';
|
||||
import { useReducedMotion } from '@/hooks/use-reduced-motion';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
|
||||
export interface ValuePoint {
|
||||
icon: LucideIcon;
|
||||
title: string;
|
||||
description: string;
|
||||
/** Optional stat number shown in corner */
|
||||
stat?: string;
|
||||
statLabel?: string;
|
||||
}
|
||||
|
||||
export interface NarrativeValueProps {
|
||||
/** Section badge */
|
||||
badge?: string;
|
||||
/** Section title */
|
||||
title: string;
|
||||
/** Title highlight (calligraphy) */
|
||||
highlight?: string;
|
||||
/** Subtitle */
|
||||
subtitle?: string;
|
||||
/** Value points */
|
||||
points: ValuePoint[];
|
||||
/** Grid columns */
|
||||
columns?: 2 | 3 | 4 | 5;
|
||||
/** Background variant */
|
||||
bgVariant?: 'default' | 'section';
|
||||
}
|
||||
|
||||
export function NarrativeValue({
|
||||
badge,
|
||||
title,
|
||||
highlight,
|
||||
subtitle,
|
||||
points,
|
||||
columns = 3,
|
||||
bgVariant = 'section',
|
||||
}: NarrativeValueProps) {
|
||||
const shouldReduceMotion = useReducedMotion();
|
||||
|
||||
const gridCols = {
|
||||
2: 'md:grid-cols-2',
|
||||
3: 'md:grid-cols-2 lg:grid-cols-3',
|
||||
4: 'md:grid-cols-2 lg:grid-cols-4',
|
||||
5: 'md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5',
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`relative section-padding overflow-hidden ${bgVariant === 'section' ? 'bg-[var(--color-bg-section)]' : 'bg-[var(--color-bg-primary)]'}`}>
|
||||
<InkWashBackground variant="subtle" />
|
||||
|
||||
<div className="container-wide relative z-10">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-100px' }}
|
||||
transition={{ duration: 0.6, ease: [0.25, 1, 0.5, 1] }}
|
||||
className="mb-14"
|
||||
>
|
||||
{badge && (
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full bg-[var(--color-brand-primary-bg)] border border-[var(--color-brand-primary)]/10 mb-4">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-[var(--color-brand-primary)]" />
|
||||
<span className="text-xs font-medium tracking-wider text-[var(--color-brand-primary)]">{badge}</span>
|
||||
</div>
|
||||
)}
|
||||
<h2 className="text-3xl sm:text-4xl font-semibold text-[var(--color-text-primary)] mb-4">
|
||||
{title}
|
||||
{highlight && <span className="text-[var(--color-brand-primary)] font-calligraphy ml-1">{highlight}</span>}
|
||||
</h2>
|
||||
{subtitle && (
|
||||
<p className="text-base text-[var(--color-text-muted)] max-w-2xl">{subtitle}</p>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Value cards grid */}
|
||||
<div className={`grid grid-cols-1 ${gridCols[columns]} gap-6 md:gap-8`}>
|
||||
{points.map((point, idx) => {
|
||||
const Icon = point.icon;
|
||||
return (
|
||||
<motion.div
|
||||
key={point.title}
|
||||
initial={shouldReduceMotion ? {} : { opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: '-60px' }}
|
||||
transition={{ duration: 0.5, delay: idx * 0.08, ease: [0.25, 1, 0.5, 1] }}
|
||||
>
|
||||
<InkCard padding="md" className="h-full">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="w-11 h-11 rounded-xl flex items-center justify-center bg-[var(--color-brand-primary-bg)]">
|
||||
<Icon className="w-5 h-5 text-[var(--color-brand-primary)]" strokeWidth={1.8} />
|
||||
</div>
|
||||
{point.stat && (
|
||||
<div className="text-right">
|
||||
<div className="text-2xl font-bold text-[var(--color-brand-primary)] leading-none">{point.stat}</div>
|
||||
{point.statLabel && (
|
||||
<div className="text-[10px] text-[var(--color-text-subtle)] mt-0.5">{point.statLabel}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="text-base font-semibold text-[var(--color-text-primary)] mb-2">{point.title}</h3>
|
||||
<p className="text-sm text-[var(--color-text-muted)] leading-relaxed">{point.description}</p>
|
||||
</InkCard>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export { NarrativeHero } from './NarrativeHero';
|
||||
export type { NarrativeHeroProps } from './NarrativeHero';
|
||||
export { NarrativeValue } from './NarrativeValue';
|
||||
export type { NarrativeValueProps, ValuePoint } from './NarrativeValue';
|
||||
export { NarrativeTrust } from './NarrativeTrust';
|
||||
export type { NarrativeTrustProps } from './NarrativeTrust';
|
||||
export { NarrativeCTA } from './NarrativeCTA';
|
||||
export type { NarrativeCTAProps } from './NarrativeCTA';
|
||||
Reference in New Issue
Block a user