@m13v/seo-components 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +52 -0
- package/src/components/AnimatedBeam.tsx +273 -0
- package/src/components/AnimatedChecklist.tsx +68 -0
- package/src/components/AnimatedCodeBlock.tsx +68 -0
- package/src/components/AnimatedDemo.tsx +186 -0
- package/src/components/AnimatedMetric.tsx +54 -0
- package/src/components/AnimatedSection.tsx +31 -0
- package/src/components/ArticleMeta.tsx +51 -0
- package/src/components/BackgroundGrid.tsx +59 -0
- package/src/components/BeforeAfter.tsx +121 -0
- package/src/components/BentoGrid.tsx +67 -0
- package/src/components/Breadcrumbs.tsx +40 -0
- package/src/components/CodeComparison.tsx +115 -0
- package/src/components/ComparisonTable.tsx +66 -0
- package/src/components/FaqSection.tsx +40 -0
- package/src/components/FlowDiagram.tsx +86 -0
- package/src/components/GlowCard.tsx +58 -0
- package/src/components/GradientText.tsx +43 -0
- package/src/components/InlineCta.tsx +50 -0
- package/src/components/InlineTestimonial.tsx +53 -0
- package/src/components/LottiePlayer.tsx +63 -0
- package/src/components/Marquee.tsx +67 -0
- package/src/components/MetricsRow.tsx +32 -0
- package/src/components/MorphingText.tsx +133 -0
- package/src/components/MotionSequence.tsx +150 -0
- package/src/components/NumberTicker.tsx +68 -0
- package/src/components/OrbitingCircles.tsx +83 -0
- package/src/components/ParallaxSection.tsx +56 -0
- package/src/components/Particles.tsx +268 -0
- package/src/components/ProofBand.tsx +66 -0
- package/src/components/ProofBanner.tsx +31 -0
- package/src/components/RemotionClip.tsx +216 -0
- package/src/components/SequenceDiagram.tsx +144 -0
- package/src/components/ShimmerButton.tsx +51 -0
- package/src/components/ShineBorder.tsx +87 -0
- package/src/components/StepTimeline.tsx +108 -0
- package/src/components/StickyBottomCta.tsx +53 -0
- package/src/components/TerminalOutput.tsx +93 -0
- package/src/components/TextShimmer.tsx +59 -0
- package/src/components/TypingAnimation.tsx +54 -0
- package/src/index.ts +69 -0
- package/src/lib/json-ld.ts +116 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { motion, useInView } from "framer-motion";
|
|
4
|
+
import { useRef, useState, useEffect } from "react";
|
|
5
|
+
|
|
6
|
+
interface TerminalLine {
|
|
7
|
+
text: string;
|
|
8
|
+
type?: "command" | "output" | "success" | "error" | "info";
|
|
9
|
+
delay?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface TerminalOutputProps {
|
|
13
|
+
lines: TerminalLine[];
|
|
14
|
+
title?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const typeColors: Record<string, string> = {
|
|
18
|
+
command: "text-emerald-400",
|
|
19
|
+
output: "text-zinc-400",
|
|
20
|
+
success: "text-emerald-500",
|
|
21
|
+
error: "text-red-400",
|
|
22
|
+
info: "text-amber-400",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const prefixes: Record<string, string> = {
|
|
26
|
+
command: "$ ",
|
|
27
|
+
output: " ",
|
|
28
|
+
success: "\u2713 ",
|
|
29
|
+
error: "\u2717 ",
|
|
30
|
+
info: "\u2139 ",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function TerminalOutput({ lines, title = "Terminal" }: TerminalOutputProps) {
|
|
34
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
35
|
+
const isInView = useInView(ref, { once: true, margin: "-40px" });
|
|
36
|
+
const [visibleCount, setVisibleCount] = useState(0);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isInView) return;
|
|
40
|
+
let i = 0;
|
|
41
|
+
const showNext = () => {
|
|
42
|
+
i++;
|
|
43
|
+
setVisibleCount(i);
|
|
44
|
+
if (i < lines.length) {
|
|
45
|
+
const nextDelay = lines[i]?.delay ?? (lines[i]?.type === "command" ? 400 : 120);
|
|
46
|
+
setTimeout(showNext, nextDelay);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const firstDelay = lines[0]?.delay ?? 300;
|
|
50
|
+
setTimeout(showNext, firstDelay);
|
|
51
|
+
}, [isInView, lines]);
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<motion.div
|
|
55
|
+
ref={ref}
|
|
56
|
+
className="my-8 rounded-2xl border border-zinc-800 bg-[#0d1117] overflow-hidden"
|
|
57
|
+
initial={{ opacity: 0, y: 12 }}
|
|
58
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
59
|
+
viewport={{ once: true }}
|
|
60
|
+
transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
|
|
61
|
+
>
|
|
62
|
+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-zinc-800">
|
|
63
|
+
<div className="flex gap-1.5">
|
|
64
|
+
<span className="w-2.5 h-2.5 rounded-full bg-red-400/60" />
|
|
65
|
+
<span className="w-2.5 h-2.5 rounded-full bg-amber-400/60" />
|
|
66
|
+
<span className="w-2.5 h-2.5 rounded-full bg-emerald-400/60" />
|
|
67
|
+
</div>
|
|
68
|
+
<span className="text-[11px] font-mono text-[#8b949e] ml-1">{title}</span>
|
|
69
|
+
</div>
|
|
70
|
+
<div className="p-4 font-mono text-sm min-h-[120px]">
|
|
71
|
+
{lines.slice(0, visibleCount).map((line, i) => (
|
|
72
|
+
<motion.div
|
|
73
|
+
key={i}
|
|
74
|
+
initial={{ opacity: 0, y: 4 }}
|
|
75
|
+
animate={{ opacity: 1, y: 0 }}
|
|
76
|
+
transition={{ duration: 0.15 }}
|
|
77
|
+
className={`${typeColors[line.type || "output"]} leading-relaxed`}
|
|
78
|
+
>
|
|
79
|
+
<span className="opacity-50">{prefixes[line.type || "output"]}</span>
|
|
80
|
+
{line.text}
|
|
81
|
+
</motion.div>
|
|
82
|
+
))}
|
|
83
|
+
{visibleCount < lines.length && isInView && (
|
|
84
|
+
<motion.span
|
|
85
|
+
className="inline-block w-[7px] h-[14px] bg-emerald-500 align-middle"
|
|
86
|
+
animate={{ opacity: [1, 0] }}
|
|
87
|
+
transition={{ duration: 0.6, repeat: Infinity, repeatType: "reverse" }}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</motion.div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CSSProperties, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TextShimmer: renders inline text with a teal shimmer highlight that
|
|
7
|
+
* sweeps across continuously. Useful for drawing attention to a tagline,
|
|
8
|
+
* status label, or short phrase.
|
|
9
|
+
*
|
|
10
|
+
* Requires the `shimmer` keyframe in your Tailwind config:
|
|
11
|
+
* ```
|
|
12
|
+
* keyframes: {
|
|
13
|
+
* shimmer: {
|
|
14
|
+
* "0%, 90%, 100%": {
|
|
15
|
+
* "background-position": "calc(-100% - var(--shimmer-width)) 0",
|
|
16
|
+
* },
|
|
17
|
+
* "30%, 60%": {
|
|
18
|
+
* "background-position": "calc(100% + var(--shimmer-width)) 0",
|
|
19
|
+
* },
|
|
20
|
+
* },
|
|
21
|
+
* },
|
|
22
|
+
* animation: { shimmer: "shimmer 8s infinite" }
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <TextShimmer>Now in beta</TextShimmer>
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
interface TextShimmerProps {
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
className?: string;
|
|
32
|
+
/** Width of the shimmer highlight in pixels. */
|
|
33
|
+
shimmerWidth?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function TextShimmer({
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
shimmerWidth = 100,
|
|
40
|
+
}: TextShimmerProps) {
|
|
41
|
+
const base = [
|
|
42
|
+
"mx-auto max-w-md text-zinc-500",
|
|
43
|
+
"animate-shimmer bg-clip-text bg-no-repeat [background-position:0_0] [background-size:var(--shimmer-width)_100%] [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
|
|
44
|
+
"bg-gradient-to-r from-zinc-100 via-teal-600/80 via-50% to-zinc-100",
|
|
45
|
+
].join(" ");
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<p
|
|
49
|
+
style={
|
|
50
|
+
{
|
|
51
|
+
"--shimmer-width": `${shimmerWidth}px`,
|
|
52
|
+
} as CSSProperties
|
|
53
|
+
}
|
|
54
|
+
className={className ? `${base} ${className}` : base}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</p>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* TypingAnimation: displays a string character by character, simulating a
|
|
7
|
+
* typewriter effect. Good for hero headlines, terminal-style intros, or
|
|
8
|
+
* any text you want to reveal progressively.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <TypingAnimation text="Build and ship in minutes" duration={80} />
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface TypingAnimationProps {
|
|
15
|
+
/** The full string to type out. */
|
|
16
|
+
text: string;
|
|
17
|
+
/** Milliseconds between each character. Defaults to 200. */
|
|
18
|
+
duration?: number;
|
|
19
|
+
/** Additional Tailwind classes. */
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function TypingAnimation({
|
|
24
|
+
text,
|
|
25
|
+
duration = 200,
|
|
26
|
+
className,
|
|
27
|
+
}: TypingAnimationProps) {
|
|
28
|
+
const [displayedText, setDisplayedText] = useState<string>("");
|
|
29
|
+
const [i, setI] = useState<number>(0);
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
const typingEffect = setInterval(() => {
|
|
33
|
+
if (i < text.length) {
|
|
34
|
+
setDisplayedText((prev) => prev + text.charAt(i));
|
|
35
|
+
setI(i + 1);
|
|
36
|
+
} else {
|
|
37
|
+
clearInterval(typingEffect);
|
|
38
|
+
}
|
|
39
|
+
}, duration);
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
clearInterval(typingEffect);
|
|
43
|
+
};
|
|
44
|
+
}, [duration, i, text]);
|
|
45
|
+
|
|
46
|
+
const base =
|
|
47
|
+
"font-display text-center text-4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm text-zinc-900";
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<h1 className={className ? `${base} ${className}` : base}>
|
|
51
|
+
{displayedText ? displayedText : text}
|
|
52
|
+
</h1>
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// JSON-LD structured data helpers
|
|
2
|
+
export {
|
|
3
|
+
breadcrumbListSchema,
|
|
4
|
+
faqPageSchema,
|
|
5
|
+
articleSchema,
|
|
6
|
+
howToSchema,
|
|
7
|
+
} from "./lib/json-ld";
|
|
8
|
+
export type {
|
|
9
|
+
BreadcrumbItem,
|
|
10
|
+
FaqItem,
|
|
11
|
+
ArticleSchemaInput,
|
|
12
|
+
HowToStepInput,
|
|
13
|
+
} from "./lib/json-ld";
|
|
14
|
+
|
|
15
|
+
// Trust-signal components
|
|
16
|
+
export { Breadcrumbs } from "./components/Breadcrumbs";
|
|
17
|
+
export type { BreadcrumbCrumb } from "./components/Breadcrumbs";
|
|
18
|
+
export { ArticleMeta } from "./components/ArticleMeta";
|
|
19
|
+
export { ProofBand } from "./components/ProofBand";
|
|
20
|
+
export { FaqSection } from "./components/FaqSection";
|
|
21
|
+
export { InlineTestimonial } from "./components/InlineTestimonial";
|
|
22
|
+
export { ComparisonTable } from "./components/ComparisonTable";
|
|
23
|
+
export type { ComparisonRow } from "./components/ComparisonTable";
|
|
24
|
+
|
|
25
|
+
// Content display components
|
|
26
|
+
export { AnimatedCodeBlock } from "./components/AnimatedCodeBlock";
|
|
27
|
+
export { TerminalOutput } from "./components/TerminalOutput";
|
|
28
|
+
export { FlowDiagram } from "./components/FlowDiagram";
|
|
29
|
+
export { SequenceDiagram } from "./components/SequenceDiagram";
|
|
30
|
+
export { CodeComparison } from "./components/CodeComparison";
|
|
31
|
+
export { AnimatedChecklist } from "./components/AnimatedChecklist";
|
|
32
|
+
export { AnimatedSection } from "./components/AnimatedSection";
|
|
33
|
+
export { AnimatedMetric } from "./components/AnimatedMetric";
|
|
34
|
+
export { MetricsRow } from "./components/MetricsRow";
|
|
35
|
+
|
|
36
|
+
// CTA components
|
|
37
|
+
export { InlineCta } from "./components/InlineCta";
|
|
38
|
+
export { StickyBottomCta } from "./components/StickyBottomCta";
|
|
39
|
+
|
|
40
|
+
// Proof / social proof
|
|
41
|
+
export { ProofBanner } from "./components/ProofBanner";
|
|
42
|
+
|
|
43
|
+
// Rich layout components
|
|
44
|
+
export { BentoGrid } from "./components/BentoGrid";
|
|
45
|
+
export type { BentoCard } from "./components/BentoGrid";
|
|
46
|
+
export { BeforeAfter } from "./components/BeforeAfter";
|
|
47
|
+
export { AnimatedDemo } from "./components/AnimatedDemo";
|
|
48
|
+
export { GlowCard } from "./components/GlowCard";
|
|
49
|
+
export { ParallaxSection } from "./components/ParallaxSection";
|
|
50
|
+
export { StepTimeline } from "./components/StepTimeline";
|
|
51
|
+
export { MotionSequence } from "./components/MotionSequence";
|
|
52
|
+
|
|
53
|
+
// Remotion video + Lottie
|
|
54
|
+
export { RemotionClip, ConceptReveal } from "./components/RemotionClip";
|
|
55
|
+
export { LottiePlayer } from "./components/LottiePlayer";
|
|
56
|
+
|
|
57
|
+
// Magic UI style components
|
|
58
|
+
export { Marquee } from "./components/Marquee";
|
|
59
|
+
export { AnimatedBeam } from "./components/AnimatedBeam";
|
|
60
|
+
export { OrbitingCircles } from "./components/OrbitingCircles";
|
|
61
|
+
export { NumberTicker } from "./components/NumberTicker";
|
|
62
|
+
export { ShimmerButton } from "./components/ShimmerButton";
|
|
63
|
+
export { GradientText } from "./components/GradientText";
|
|
64
|
+
export { BackgroundGrid } from "./components/BackgroundGrid";
|
|
65
|
+
export { MorphingText } from "./components/MorphingText";
|
|
66
|
+
export { Particles } from "./components/Particles";
|
|
67
|
+
export { ShineBorder } from "./components/ShineBorder";
|
|
68
|
+
export { TextShimmer } from "./components/TextShimmer";
|
|
69
|
+
export { TypingAnimation } from "./components/TypingAnimation";
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared JSON-LD structured data helpers.
|
|
3
|
+
* Product-agnostic: caller provides publisher name, URL, logo, author, etc.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface BreadcrumbItem {
|
|
7
|
+
name: string;
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FaqItem {
|
|
12
|
+
q: string;
|
|
13
|
+
a: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function breadcrumbListSchema(items: BreadcrumbItem[]) {
|
|
17
|
+
return {
|
|
18
|
+
"@context": "https://schema.org",
|
|
19
|
+
"@type": "BreadcrumbList",
|
|
20
|
+
itemListElement: items.map((item, i) => ({
|
|
21
|
+
"@type": "ListItem",
|
|
22
|
+
position: i + 1,
|
|
23
|
+
name: item.name,
|
|
24
|
+
item: item.url,
|
|
25
|
+
})),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function faqPageSchema(faqs: FaqItem[], id?: string) {
|
|
30
|
+
return {
|
|
31
|
+
"@context": "https://schema.org",
|
|
32
|
+
"@type": "FAQPage",
|
|
33
|
+
...(id ? { "@id": id } : {}),
|
|
34
|
+
mainEntity: faqs.map((f) => ({
|
|
35
|
+
"@type": "Question",
|
|
36
|
+
name: f.q,
|
|
37
|
+
acceptedAnswer: {
|
|
38
|
+
"@type": "Answer",
|
|
39
|
+
text: f.a,
|
|
40
|
+
},
|
|
41
|
+
})),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ArticleSchemaInput {
|
|
46
|
+
headline: string;
|
|
47
|
+
description: string;
|
|
48
|
+
url: string;
|
|
49
|
+
datePublished: string;
|
|
50
|
+
dateModified?: string;
|
|
51
|
+
author: string;
|
|
52
|
+
authorUrl?: string;
|
|
53
|
+
publisherName: string;
|
|
54
|
+
publisherUrl: string;
|
|
55
|
+
publisherLogo?: string;
|
|
56
|
+
image?: string;
|
|
57
|
+
articleType?: "Article" | "BlogPosting" | "TechArticle";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function articleSchema({
|
|
61
|
+
headline,
|
|
62
|
+
description,
|
|
63
|
+
url,
|
|
64
|
+
datePublished,
|
|
65
|
+
dateModified,
|
|
66
|
+
author,
|
|
67
|
+
authorUrl,
|
|
68
|
+
publisherName,
|
|
69
|
+
publisherUrl,
|
|
70
|
+
publisherLogo,
|
|
71
|
+
image,
|
|
72
|
+
articleType = "Article",
|
|
73
|
+
}: ArticleSchemaInput) {
|
|
74
|
+
return {
|
|
75
|
+
"@context": "https://schema.org",
|
|
76
|
+
"@type": articleType,
|
|
77
|
+
headline,
|
|
78
|
+
description,
|
|
79
|
+
datePublished,
|
|
80
|
+
dateModified: dateModified || datePublished,
|
|
81
|
+
author: {
|
|
82
|
+
"@type": "Person",
|
|
83
|
+
name: author,
|
|
84
|
+
...(authorUrl ? { url: authorUrl } : {}),
|
|
85
|
+
},
|
|
86
|
+
publisher: {
|
|
87
|
+
"@type": "Organization",
|
|
88
|
+
name: publisherName,
|
|
89
|
+
url: publisherUrl,
|
|
90
|
+
...(publisherLogo ? { logo: publisherLogo } : {}),
|
|
91
|
+
},
|
|
92
|
+
url,
|
|
93
|
+
mainEntityOfPage: url,
|
|
94
|
+
...(image ? { image } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface HowToStepInput {
|
|
99
|
+
name: string;
|
|
100
|
+
text: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function howToSchema(name: string, description: string, steps: HowToStepInput[]) {
|
|
104
|
+
return {
|
|
105
|
+
"@context": "https://schema.org",
|
|
106
|
+
"@type": "HowTo",
|
|
107
|
+
name,
|
|
108
|
+
description,
|
|
109
|
+
step: steps.map((s, i) => ({
|
|
110
|
+
"@type": "HowToStep",
|
|
111
|
+
position: i + 1,
|
|
112
|
+
name: s.name,
|
|
113
|
+
text: s.text,
|
|
114
|
+
})),
|
|
115
|
+
};
|
|
116
|
+
}
|