@stampui/blocks 1.0.0 → 1.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.
@@ -0,0 +1,196 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Github, Twitter, MessageSquare, Linkedin } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface FooterLinkItem {
8
+ label: string
9
+ href: string
10
+ }
11
+
12
+ export interface FooterLinkGroup {
13
+ title: string
14
+ items: FooterLinkItem[]
15
+ }
16
+
17
+ export interface FooterLink {
18
+ label: string
19
+ href: string
20
+ groups?: FooterLinkGroup[]
21
+ }
22
+
23
+ export type SocialPlatform = "github" | "twitter" | "discord" | "linkedin"
24
+
25
+ export interface FooterSocial {
26
+ icon: SocialPlatform
27
+ href: string
28
+ }
29
+
30
+ export interface SiteFooterProps {
31
+ brand?: { name: string; href?: string }
32
+ links?: FooterLink[]
33
+ socials?: FooterSocial[]
34
+ copyright?: string
35
+ className?: string
36
+ }
37
+
38
+ const SOCIAL_ICONS: Record<SocialPlatform, React.ReactNode> = {
39
+ github: <Github size={16} />,
40
+ twitter: <Twitter size={16} />,
41
+ discord: <MessageSquare size={16} />,
42
+ linkedin: <Linkedin size={16} />,
43
+ }
44
+
45
+ const DEFAULT_LINKS: FooterLink[] = [
46
+ {
47
+ label: "Product",
48
+ href: "#",
49
+ groups: [
50
+ {
51
+ title: "Product",
52
+ items: [
53
+ { label: "Blocks", href: "/blocks" },
54
+ { label: "Components", href: "/blocks/components" },
55
+ { label: "Changelog", href: "/changelog" },
56
+ { label: "Roadmap", href: "/roadmap" },
57
+ ],
58
+ },
59
+ ],
60
+ },
61
+ {
62
+ label: "Developers",
63
+ href: "#",
64
+ groups: [
65
+ {
66
+ title: "Developers",
67
+ items: [
68
+ { label: "Documentation", href: "/docs" },
69
+ { label: "CLI Reference", href: "/docs/cli" },
70
+ { label: "GitHub", href: "https://github.com" },
71
+ ],
72
+ },
73
+ ],
74
+ },
75
+ {
76
+ label: "Company",
77
+ href: "#",
78
+ groups: [
79
+ {
80
+ title: "Company",
81
+ items: [
82
+ { label: "About", href: "/about" },
83
+ { label: "Blog", href: "/blog" },
84
+ { label: "Privacy", href: "/privacy" },
85
+ { label: "Terms", href: "/terms" },
86
+ ],
87
+ },
88
+ ],
89
+ },
90
+ ]
91
+
92
+ function resolveColumns(links: FooterLink[]): FooterLinkGroup[] {
93
+ const cols: FooterLinkGroup[] = []
94
+ for (const link of links) {
95
+ if (link.groups && link.groups.length > 0) {
96
+ cols.push(...link.groups)
97
+ } else {
98
+ cols.push({ title: link.label, items: [{ label: link.label, href: link.href }] })
99
+ }
100
+ }
101
+ return cols
102
+ }
103
+
104
+ export function SiteFooter({
105
+ brand = { name: "StampUI", href: "/" },
106
+ links = DEFAULT_LINKS,
107
+ socials = [
108
+ { icon: "github", href: "https://github.com" },
109
+ { icon: "twitter", href: "https://twitter.com" },
110
+ { icon: "discord", href: "https://discord.com" },
111
+ ],
112
+ copyright,
113
+ className,
114
+ }: SiteFooterProps) {
115
+ const columns = resolveColumns(links)
116
+ const year = new Date().getFullYear()
117
+
118
+ return (
119
+ <footer
120
+ className={cx(
121
+ "w-full border-t border-[#23252A] bg-[#070708] px-6 pt-14 pb-10",
122
+ className
123
+ )}
124
+ >
125
+ <div className="max-w-5xl mx-auto">
126
+ <div className="grid grid-cols-2 sm:grid-cols-[auto_1fr] gap-10 md:gap-16">
127
+ <div className="col-span-2 sm:col-span-1 flex flex-col gap-4">
128
+ {brand.href ? (
129
+ <a
130
+ href={brand.href}
131
+ className="text-sm font-semibold text-[#FAFAFA] transition-colors duration-[170ms] ease-out hover:text-[#FAFAFA]/70"
132
+ >
133
+ {brand.name}
134
+ </a>
135
+ ) : (
136
+ <span className="text-sm font-semibold text-[#FAFAFA]">{brand.name}</span>
137
+ )}
138
+
139
+ {socials && socials.length > 0 && (
140
+ <div className="flex items-center gap-3">
141
+ {socials.map((s, i) => (
142
+ <a
143
+ key={i}
144
+ href={s.href}
145
+ target="_blank"
146
+ rel="noopener noreferrer"
147
+ className={cx(
148
+ "flex items-center justify-center w-8 h-8 rounded-xl",
149
+ "border border-[#23252A] bg-[#09090B] text-muted-foreground",
150
+ "transition-colors duration-[170ms] ease-out",
151
+ "hover:border-[#FAFAFA]/20 hover:text-[#FAFAFA]"
152
+ )}
153
+ >
154
+ {SOCIAL_ICONS[s.icon]}
155
+ </a>
156
+ ))}
157
+ </div>
158
+ )}
159
+ </div>
160
+
161
+ <div className="col-span-2 sm:col-span-1 grid grid-cols-2 sm:grid-cols-3 gap-8">
162
+ {columns.map((col, i) => (
163
+ <div key={i} className="flex flex-col gap-3">
164
+ <span className="text-xs font-semibold text-[#FAFAFA] uppercase tracking-wider">
165
+ {col.title}
166
+ </span>
167
+ <ul className="flex flex-col gap-2">
168
+ {col.items.map((item, j) => (
169
+ <li key={j}>
170
+ <a
171
+ href={item.href}
172
+ className={cx(
173
+ "text-sm text-muted-foreground",
174
+ "transition-colors duration-[170ms] ease-out",
175
+ "hover:text-[#FAFAFA]"
176
+ )}
177
+ >
178
+ {item.label}
179
+ </a>
180
+ </li>
181
+ ))}
182
+ </ul>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ </div>
187
+
188
+ <div className="mt-12 pt-6 border-t border-[#23252A]">
189
+ <p className="text-xs text-muted-foreground">
190
+ {copyright ?? `© ${year} ${brand.name}. All rights reserved.`}
191
+ </p>
192
+ </div>
193
+ </div>
194
+ </footer>
195
+ )
196
+ }
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ export interface LogoItem {
7
+ name: string
8
+ src?: string
9
+ href?: string
10
+ }
11
+
12
+ export interface SocialProofBarProps {
13
+ label?: string
14
+ logos?: LogoItem[]
15
+ className?: string
16
+ }
17
+
18
+ const defaultLogos: LogoItem[] = [
19
+ { name: "Vercel" },
20
+ { name: "Linear" },
21
+ { name: "Resend" },
22
+ { name: "PlanetScale" },
23
+ { name: "Railway" },
24
+ { name: "Liveblocks" },
25
+ ]
26
+
27
+ function LogoEntry({ item }: { item: LogoItem }) {
28
+ const inner = item.src ? (
29
+ <img
30
+ src={item.src}
31
+ alt={item.name}
32
+ className="h-5 w-auto object-contain opacity-50 transition-opacity duration-150 ease-out group-hover:opacity-75"
33
+ />
34
+ ) : (
35
+ <span className="text-xs font-semibold tracking-wide text-muted-foreground opacity-60 transition-opacity duration-150 ease-out group-hover:opacity-90">
36
+ {item.name}
37
+ </span>
38
+ )
39
+
40
+ if (item.href) {
41
+ return (
42
+ <a
43
+ href={item.href}
44
+ target="_blank"
45
+ rel="noopener noreferrer"
46
+ className="group flex items-center"
47
+ aria-label={item.name}
48
+ >
49
+ {inner}
50
+ </a>
51
+ )
52
+ }
53
+
54
+ return <div className="group flex items-center">{inner}</div>
55
+ }
56
+
57
+ export function SocialProofBar({
58
+ label = "Trusted by teams at",
59
+ logos = defaultLogos,
60
+ className,
61
+ }: SocialProofBarProps) {
62
+ return (
63
+ <div className={cx("w-full", className)}>
64
+ <div className="border-t border-[#23252A]" />
65
+ <div className="mx-auto flex max-w-5xl flex-col items-center gap-5 px-6 py-8 sm:flex-row sm:gap-8">
66
+ {label && (
67
+ <p className="shrink-0 text-xs text-muted-foreground opacity-60 sm:whitespace-nowrap">
68
+ {label}
69
+ </p>
70
+ )}
71
+ {label && (
72
+ <div className="hidden h-4 w-px bg-[#23252A] sm:block" aria-hidden />
73
+ )}
74
+ <div className="flex flex-wrap items-center justify-center gap-x-8 gap-y-4 sm:justify-start">
75
+ {logos.map((logo, i) => (
76
+ <LogoEntry key={i} item={logo} />
77
+ ))}
78
+ </div>
79
+ </div>
80
+ <div className="border-b border-[#23252A]" />
81
+ </div>
82
+ )
83
+ }
@@ -0,0 +1,167 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Github, Twitter, Linkedin } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface TeamMember {
8
+ name: string
9
+ role: string
10
+ bio?: string
11
+ avatarUrl?: string
12
+ socials?: {
13
+ github?: string
14
+ twitter?: string
15
+ linkedin?: string
16
+ }
17
+ }
18
+
19
+ export interface TeamGridProps {
20
+ members: TeamMember[]
21
+ title?: string
22
+ columns?: 2 | 3 | 4
23
+ className?: string
24
+ }
25
+
26
+ const colClass: Record<2 | 3 | 4, string> = {
27
+ 2: "sm:grid-cols-2",
28
+ 3: "sm:grid-cols-2 lg:grid-cols-3",
29
+ 4: "sm:grid-cols-2 lg:grid-cols-4",
30
+ }
31
+
32
+ function getInitials(name: string): string {
33
+ return name
34
+ .split(" ")
35
+ .map((n) => n[0])
36
+ .join("")
37
+ .toUpperCase()
38
+ .slice(0, 2)
39
+ }
40
+
41
+ const defaultMembers: TeamMember[] = [
42
+ {
43
+ name: "Aria Voss",
44
+ role: "Engineering Lead",
45
+ bio: "Builds distributed systems and spends too much time in the terminal.",
46
+ socials: { github: "#", twitter: "#", linkedin: "#" },
47
+ },
48
+ {
49
+ name: "Tomás Reyes",
50
+ role: "Product Designer",
51
+ bio: "Turns complex workflows into calm, focused interfaces.",
52
+ socials: { github: "#", linkedin: "#" },
53
+ },
54
+ {
55
+ name: "Lena Krüger",
56
+ role: "Developer Advocate",
57
+ bio: "Writes docs, builds demos, and advocates for sensible APIs.",
58
+ socials: { github: "#", twitter: "#" },
59
+ },
60
+ {
61
+ name: "Jin Park",
62
+ role: "Infrastructure",
63
+ bio: "Keeps the lights on and the deploys green.",
64
+ socials: { github: "#", linkedin: "#" },
65
+ },
66
+ {
67
+ name: "Priya Nair",
68
+ role: "Frontend Engineer",
69
+ bio: "Crafts accessible, performant component systems.",
70
+ socials: { github: "#", twitter: "#", linkedin: "#" },
71
+ },
72
+ {
73
+ name: "Sam Okafor",
74
+ role: "Backend Engineer",
75
+ bio: "API design, data modeling, and strong opinions about naming.",
76
+ socials: { github: "#" },
77
+ },
78
+ ]
79
+
80
+ export function TeamGrid({
81
+ members = defaultMembers,
82
+ title,
83
+ columns = 3,
84
+ className,
85
+ }: TeamGridProps) {
86
+ return (
87
+ <section className={cx("w-full py-16 px-6 bg-[#070708]", className)}>
88
+ <div className="mx-auto max-w-5xl">
89
+ {title && (
90
+ <h2 className="text-2xl font-semibold text-[#FAFAFA] tracking-tight mb-10">
91
+ {title}
92
+ </h2>
93
+ )}
94
+ <div className={cx("grid grid-cols-1 gap-4", colClass[columns])}>
95
+ {members.map((member, i) => (
96
+ <div
97
+ key={i}
98
+ className="flex flex-col gap-4 rounded-xl border border-[#23252A] bg-[#09090B] p-5 transition-colors duration-[180ms] ease-out hover:border-[#2e3138] hover:bg-[#101114]"
99
+ >
100
+ <div className="flex items-center gap-3">
101
+ {member.avatarUrl ? (
102
+ <img
103
+ src={member.avatarUrl}
104
+ alt={member.name}
105
+ className="h-10 w-10 rounded-xl object-cover border border-[#23252A] shrink-0"
106
+ />
107
+ ) : (
108
+ <div className="h-10 w-10 rounded-xl border border-[#23252A] bg-[#101114] flex items-center justify-center shrink-0">
109
+ <span className="text-xs font-semibold text-muted-foreground">
110
+ {getInitials(member.name)}
111
+ </span>
112
+ </div>
113
+ )}
114
+ <div className="min-w-0">
115
+ <p className="text-sm font-medium text-[#FAFAFA] truncate">{member.name}</p>
116
+ <p className="text-xs text-muted-foreground truncate">{member.role}</p>
117
+ </div>
118
+ </div>
119
+
120
+ {member.bio && (
121
+ <p className="text-xs text-muted-foreground leading-relaxed">{member.bio}</p>
122
+ )}
123
+
124
+ {member.socials && (
125
+ <div className="flex items-center gap-2 mt-auto pt-1">
126
+ {member.socials.github && (
127
+ <a
128
+ href={member.socials.github}
129
+ target="_blank"
130
+ rel="noopener noreferrer"
131
+ className="text-muted-foreground transition-colors duration-[150ms] ease-out hover:text-[#FAFAFA]"
132
+ aria-label={`${member.name} on GitHub`}
133
+ >
134
+ <Github size={14} />
135
+ </a>
136
+ )}
137
+ {member.socials.twitter && (
138
+ <a
139
+ href={member.socials.twitter}
140
+ target="_blank"
141
+ rel="noopener noreferrer"
142
+ className="text-muted-foreground transition-colors duration-[150ms] ease-out hover:text-[#FAFAFA]"
143
+ aria-label={`${member.name} on Twitter`}
144
+ >
145
+ <Twitter size={14} />
146
+ </a>
147
+ )}
148
+ {member.socials.linkedin && (
149
+ <a
150
+ href={member.socials.linkedin}
151
+ target="_blank"
152
+ rel="noopener noreferrer"
153
+ className="text-muted-foreground transition-colors duration-[150ms] ease-out hover:text-[#FAFAFA]"
154
+ aria-label={`${member.name} on LinkedIn`}
155
+ >
156
+ <Linkedin size={14} />
157
+ </a>
158
+ )}
159
+ </div>
160
+ )}
161
+ </div>
162
+ ))}
163
+ </div>
164
+ </div>
165
+ </section>
166
+ )
167
+ }
@@ -0,0 +1,110 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ export interface TestimonialAuthor {
7
+ name: string
8
+ role: string
9
+ avatarUrl?: string
10
+ }
11
+
12
+ export interface TestimonialItem {
13
+ quote: string
14
+ author: TestimonialAuthor
15
+ }
16
+
17
+ export interface TestimonialsWallProps {
18
+ items?: TestimonialItem[]
19
+ title?: string
20
+ className?: string
21
+ }
22
+
23
+ const defaultItems: TestimonialItem[] = [
24
+ {
25
+ quote: "StampUI cut our component scaffolding time in half. Every block lands exactly where we need it, with zero runtime overhead.",
26
+ author: { name: "Alex Rivera", role: "Staff Engineer, Vercel" },
27
+ },
28
+ {
29
+ quote: "Finally a component library that doesn't fight our design system. We own the code, we change what we want.",
30
+ author: { name: "Priya Nair", role: "Frontend Lead, Linear" },
31
+ },
32
+ {
33
+ quote: "The dark-mode polish is remarkable. We shipped our dashboard in two days instead of two weeks.",
34
+ author: { name: "Jordan Kim", role: "Product Engineer, Resend" },
35
+ },
36
+ {
37
+ quote: "I've tried every UI library. StampUI is the only one I've never had to fight against.",
38
+ author: { name: "Sam Okafor", role: "CTO, Liveblocks" },
39
+ },
40
+ {
41
+ quote: "Using the CLI from the terminal while on a call and shipping production UI. This is the workflow I wanted.",
42
+ author: { name: "Taylor Chen", role: "Senior Engineer, PlanetScale" },
43
+ },
44
+ {
45
+ quote: "Every team member can now stamp a block and own it immediately. Onboarding velocity is through the roof.",
46
+ author: { name: "Dana Walsh", role: "Engineering Manager, Railway" },
47
+ },
48
+ ]
49
+
50
+ function Initials({ name }: { name: string }) {
51
+ const parts = name.trim().split(" ")
52
+ const initials =
53
+ parts.length >= 2
54
+ ? `${parts[0][0]}${parts[parts.length - 1][0]}`
55
+ : parts[0].slice(0, 2)
56
+ return (
57
+ <span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[#101114] border border-[#23252A] text-xs font-semibold text-[#FAFAFA] select-none">
58
+ {initials.toUpperCase()}
59
+ </span>
60
+ )
61
+ }
62
+
63
+ export function TestimonialsWall({
64
+ items = defaultItems,
65
+ title,
66
+ className,
67
+ }: TestimonialsWallProps) {
68
+ return (
69
+ <section className={cx("w-full py-24 px-6", className)}>
70
+ <div className="mx-auto max-w-5xl">
71
+ {title && (
72
+ <h2 className="mb-12 text-center text-2xl font-semibold tracking-tight text-[#FAFAFA]">
73
+ {title}
74
+ </h2>
75
+ )}
76
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
77
+ {items.map((item, i) => (
78
+ <div
79
+ key={i}
80
+ className="flex flex-col justify-between gap-5 rounded-xl border border-[#23252A] bg-[#09090B] p-5"
81
+ >
82
+ <p className="text-sm leading-relaxed text-muted-foreground">
83
+ &ldquo;{item.quote}&rdquo;
84
+ </p>
85
+ <div className="flex items-center gap-3">
86
+ {item.author.avatarUrl ? (
87
+ <img
88
+ src={item.author.avatarUrl}
89
+ alt={item.author.name}
90
+ className="h-8 w-8 shrink-0 rounded-full border border-[#23252A] object-cover"
91
+ />
92
+ ) : (
93
+ <Initials name={item.author.name} />
94
+ )}
95
+ <div className="min-w-0">
96
+ <p className="truncate text-sm font-medium text-[#FAFAFA]">
97
+ {item.author.name}
98
+ </p>
99
+ <p className="truncate text-xs text-muted-foreground">
100
+ {item.author.role}
101
+ </p>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ ))}
106
+ </div>
107
+ </div>
108
+ </section>
109
+ )
110
+ }
@@ -0,0 +1,102 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Button } from "@/components/core/button"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface WaitlistSectionProps {
8
+ headline?: string
9
+ subtext?: string
10
+ placeholder?: string
11
+ buttonLabel?: string
12
+ count?: number
13
+ className?: string
14
+ }
15
+
16
+ export function WaitlistSection({
17
+ headline = "Be first to get access.",
18
+ subtext = "We're rolling out access gradually. Drop your email to reserve your spot.",
19
+ placeholder = "you@company.com",
20
+ buttonLabel = "Join Waitlist",
21
+ count = 2847,
22
+ className,
23
+ }: WaitlistSectionProps) {
24
+ const [email, setEmail] = React.useState("")
25
+ const [submitted, setSubmitted] = React.useState(false)
26
+ const [error, setError] = React.useState("")
27
+
28
+ function handleSubmit(e: React.FormEvent) {
29
+ e.preventDefault()
30
+ const trimmed = email.trim()
31
+ if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
32
+ setError("Enter a valid email address.")
33
+ return
34
+ }
35
+ setError("")
36
+ setSubmitted(true)
37
+ }
38
+
39
+ const formattedCount = count.toLocaleString()
40
+
41
+ return (
42
+ <section
43
+ className={cx(
44
+ "w-full py-20 px-6 flex flex-col items-center text-center",
45
+ className
46
+ )}
47
+ >
48
+ <h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-[#FAFAFA] max-w-xl leading-tight">
49
+ {headline}
50
+ </h2>
51
+
52
+ {subtext && (
53
+ <p className="mt-4 text-base text-muted-foreground max-w-md leading-relaxed">
54
+ {subtext}
55
+ </p>
56
+ )}
57
+
58
+ <div className="mt-3 flex items-center gap-1.5">
59
+ <span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
60
+ <span className="text-xs text-muted-foreground font-medium">
61
+ {formattedCount} developers waiting
62
+ </span>
63
+ </div>
64
+
65
+ {submitted ? (
66
+ <div className="mt-8 px-6 py-4 rounded-xl border border-[#23252A] bg-[#09090B] text-sm text-[#FAFAFA] font-medium">
67
+ You&apos;re on the list. We&apos;ll reach out soon.
68
+ </div>
69
+ ) : (
70
+ <form
71
+ onSubmit={handleSubmit}
72
+ className="mt-8 w-full max-w-md flex flex-col sm:flex-row gap-2"
73
+ noValidate
74
+ >
75
+ <div className="flex-1 flex flex-col">
76
+ <input
77
+ type="email"
78
+ value={email}
79
+ onChange={(e) => {
80
+ setEmail(e.target.value)
81
+ if (error) setError("")
82
+ }}
83
+ placeholder={placeholder}
84
+ className={cx(
85
+ "h-10 w-full rounded-xl border bg-[#101114] px-4 text-sm text-[#FAFAFA] placeholder:text-muted-foreground outline-none",
86
+ "transition-colors duration-[170ms] ease-out",
87
+ "focus:border-[#FAFAFA]/20",
88
+ error ? "border-red-500/60" : "border-[#23252A]"
89
+ )}
90
+ />
91
+ {error && (
92
+ <span className="mt-1.5 text-xs text-red-400 text-left">{error}</span>
93
+ )}
94
+ </div>
95
+ <Button type="submit" className="h-10 shrink-0">
96
+ {buttonLabel}
97
+ </Button>
98
+ </form>
99
+ )}
100
+ </section>
101
+ )
102
+ }