@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.
package/dist/manifests.js CHANGED
@@ -1662,5 +1662,272 @@ exports.manifests = {
1662
1662
  supportsLightMode: true,
1663
1663
  promptReady: true,
1664
1664
  },
1665
+ // ─── New Blocks ───────────────────────────────────────────────────────────
1666
+ "waitlist-section": {
1667
+ slug: "waitlist-section",
1668
+ title: "Waitlist Section",
1669
+ description: "Email signup section with a waitlist counter and thank-you state.",
1670
+ category: "Marketing",
1671
+ tags: ["waitlist", "email", "signup", "landing"],
1672
+ version: "1.0.0",
1673
+ updatedAt: "2026-05-17",
1674
+ changelog: ["Initial release"],
1675
+ status: "free",
1676
+ difficulty: "beginner",
1677
+ frameworks: ["nextjs", "react", "vite"],
1678
+ dependencies: [],
1679
+ files: [{ path: "components/blocks/waitlist-section.tsx", type: "block" }],
1680
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1681
+ supportsDarkMode: true,
1682
+ supportsLightMode: false,
1683
+ promptReady: true,
1684
+ },
1685
+ "changelog-feed": {
1686
+ slug: "changelog-feed",
1687
+ title: "Changelog Feed",
1688
+ description: "Vertical timeline of versioned product updates with type tags.",
1689
+ category: "Display",
1690
+ tags: ["changelog", "timeline", "updates", "versioning"],
1691
+ version: "1.0.0",
1692
+ updatedAt: "2026-05-17",
1693
+ changelog: ["Initial release"],
1694
+ status: "free",
1695
+ difficulty: "beginner",
1696
+ frameworks: ["nextjs", "react", "vite"],
1697
+ dependencies: [],
1698
+ files: [{ path: "components/blocks/changelog-feed.tsx", type: "block" }],
1699
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1700
+ supportsDarkMode: true,
1701
+ supportsLightMode: false,
1702
+ promptReady: true,
1703
+ },
1704
+ "faq-accordion": {
1705
+ slug: "faq-accordion",
1706
+ title: "FAQ Accordion",
1707
+ description: "Expandable FAQ section built without external accordion dependencies.",
1708
+ category: "Marketing",
1709
+ tags: ["faq", "accordion", "help", "landing"],
1710
+ version: "1.0.0",
1711
+ updatedAt: "2026-05-17",
1712
+ changelog: ["Initial release"],
1713
+ status: "free",
1714
+ difficulty: "beginner",
1715
+ frameworks: ["nextjs", "react", "vite"],
1716
+ dependencies: ["lucide-react"],
1717
+ files: [{ path: "components/blocks/faq-accordion.tsx", type: "block" }],
1718
+ tokens: ["foreground", "muted-foreground", "border"],
1719
+ supportsDarkMode: true,
1720
+ supportsLightMode: false,
1721
+ promptReady: true,
1722
+ },
1723
+ "cta-banner": {
1724
+ slug: "cta-banner",
1725
+ title: "CTA Banner",
1726
+ description: "End-of-page call-to-action section with primary and secondary buttons.",
1727
+ category: "Marketing",
1728
+ tags: ["cta", "call-to-action", "landing", "conversion"],
1729
+ version: "1.0.0",
1730
+ updatedAt: "2026-05-17",
1731
+ changelog: ["Initial release"],
1732
+ status: "free",
1733
+ difficulty: "beginner",
1734
+ frameworks: ["nextjs", "react", "vite"],
1735
+ dependencies: [],
1736
+ files: [{ path: "components/blocks/cta-banner.tsx", type: "block" }],
1737
+ tokens: ["foreground", "muted-foreground", "border"],
1738
+ supportsDarkMode: true,
1739
+ supportsLightMode: false,
1740
+ promptReady: true,
1741
+ },
1742
+ "site-footer": {
1743
+ slug: "site-footer",
1744
+ title: "Site Footer",
1745
+ description: "Full site footer with brand, grouped nav links, social icons, and copyright.",
1746
+ category: "Navigation",
1747
+ tags: ["footer", "navigation", "links", "social"],
1748
+ version: "1.0.0",
1749
+ updatedAt: "2026-05-17",
1750
+ changelog: ["Initial release"],
1751
+ status: "free",
1752
+ difficulty: "beginner",
1753
+ frameworks: ["nextjs", "react", "vite"],
1754
+ dependencies: ["lucide-react"],
1755
+ files: [{ path: "components/blocks/site-footer.tsx", type: "block" }],
1756
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1757
+ supportsDarkMode: true,
1758
+ supportsLightMode: false,
1759
+ promptReady: true,
1760
+ },
1761
+ "testimonials-wall": {
1762
+ slug: "testimonials-wall",
1763
+ title: "Testimonials Wall",
1764
+ description: "Responsive grid of testimonial cards with author avatars and quotes.",
1765
+ category: "Marketing",
1766
+ tags: ["testimonials", "social-proof", "quotes", "grid"],
1767
+ version: "1.0.0",
1768
+ updatedAt: "2026-05-17",
1769
+ changelog: ["Initial release"],
1770
+ status: "free",
1771
+ difficulty: "beginner",
1772
+ frameworks: ["nextjs", "react", "vite"],
1773
+ dependencies: [],
1774
+ files: [{ path: "components/blocks/testimonials-wall.tsx", type: "block" }],
1775
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1776
+ supportsDarkMode: true,
1777
+ supportsLightMode: false,
1778
+ promptReady: true,
1779
+ },
1780
+ "metrics-grid": {
1781
+ slug: "metrics-grid",
1782
+ title: "Metrics Grid",
1783
+ description: "Dashboard KPI grid with trend indicators, icons, and configurable columns.",
1784
+ category: "Dashboard",
1785
+ tags: ["metrics", "kpi", "dashboard", "stats", "analytics"],
1786
+ version: "1.0.0",
1787
+ updatedAt: "2026-05-17",
1788
+ changelog: ["Initial release"],
1789
+ status: "free",
1790
+ difficulty: "intermediate",
1791
+ frameworks: ["nextjs", "react"],
1792
+ dependencies: ["lucide-react"],
1793
+ files: [{ path: "components/blocks/metrics-grid.tsx", type: "block" }],
1794
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1795
+ supportsDarkMode: true,
1796
+ supportsLightMode: false,
1797
+ promptReady: true,
1798
+ },
1799
+ "announcement-bar": {
1800
+ slug: "announcement-bar",
1801
+ title: "Announcement Bar",
1802
+ description: "Dismissible top-of-page announcement banner with optional CTA.",
1803
+ category: "Feedback",
1804
+ tags: ["announcement", "banner", "notification", "top-bar"],
1805
+ version: "1.0.0",
1806
+ updatedAt: "2026-05-17",
1807
+ changelog: ["Initial release"],
1808
+ status: "free",
1809
+ difficulty: "beginner",
1810
+ frameworks: ["nextjs", "react", "vite"],
1811
+ dependencies: ["lucide-react"],
1812
+ files: [{ path: "components/blocks/announcement-bar.tsx", type: "block" }],
1813
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1814
+ supportsDarkMode: true,
1815
+ supportsLightMode: false,
1816
+ promptReady: true,
1817
+ },
1818
+ "social-proof-bar": {
1819
+ slug: "social-proof-bar",
1820
+ title: "Social Proof Bar",
1821
+ description: "Horizontal logo strip showing companies or teams using your product.",
1822
+ category: "Marketing",
1823
+ tags: ["logos", "social-proof", "companies", "trust"],
1824
+ version: "1.0.0",
1825
+ updatedAt: "2026-05-17",
1826
+ changelog: ["Initial release"],
1827
+ status: "free",
1828
+ difficulty: "beginner",
1829
+ frameworks: ["nextjs", "react", "vite"],
1830
+ dependencies: [],
1831
+ files: [{ path: "components/blocks/social-proof-bar.tsx", type: "block" }],
1832
+ tokens: ["muted-foreground", "border"],
1833
+ supportsDarkMode: true,
1834
+ supportsLightMode: false,
1835
+ promptReady: true,
1836
+ },
1837
+ "feature-comparison": {
1838
+ slug: "feature-comparison",
1839
+ title: "Feature Comparison",
1840
+ description: "Plan comparison table with highlighted column, checkmarks, and custom values.",
1841
+ category: "Marketing",
1842
+ tags: ["pricing", "comparison", "table", "features", "plans"],
1843
+ version: "1.0.0",
1844
+ updatedAt: "2026-05-17",
1845
+ changelog: ["Initial release"],
1846
+ status: "free",
1847
+ difficulty: "intermediate",
1848
+ frameworks: ["nextjs", "react", "vite"],
1849
+ dependencies: [],
1850
+ files: [{ path: "components/blocks/feature-comparison.tsx", type: "block" }],
1851
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1852
+ supportsDarkMode: true,
1853
+ supportsLightMode: false,
1854
+ promptReady: true,
1855
+ },
1856
+ "team-grid": {
1857
+ slug: "team-grid",
1858
+ title: "Team Grid",
1859
+ description: "Responsive grid of team member cards with avatars, roles, bios, and social links.",
1860
+ category: "Display",
1861
+ tags: ["team", "about", "members", "grid", "people"],
1862
+ version: "1.0.0",
1863
+ updatedAt: "2026-05-17",
1864
+ changelog: ["Initial release"],
1865
+ status: "free",
1866
+ difficulty: "beginner",
1867
+ frameworks: ["nextjs", "react", "vite"],
1868
+ dependencies: ["lucide-react"],
1869
+ files: [{ path: "components/blocks/team-grid.tsx", type: "block" }],
1870
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1871
+ supportsDarkMode: true,
1872
+ supportsLightMode: false,
1873
+ promptReady: true,
1874
+ },
1875
+ "empty-state": {
1876
+ slug: "empty-state",
1877
+ title: "Empty State",
1878
+ description: "Centered empty state with icon, title, description, and optional action.",
1879
+ category: "Feedback",
1880
+ tags: ["empty", "no-data", "placeholder", "state"],
1881
+ version: "1.0.0",
1882
+ updatedAt: "2026-05-17",
1883
+ changelog: ["Initial release"],
1884
+ status: "free",
1885
+ difficulty: "beginner",
1886
+ frameworks: ["nextjs", "react", "vite"],
1887
+ dependencies: ["lucide-react"],
1888
+ files: [{ path: "components/blocks/empty-state.tsx", type: "block" }],
1889
+ tokens: ["foreground", "muted-foreground", "border"],
1890
+ supportsDarkMode: true,
1891
+ supportsLightMode: false,
1892
+ promptReady: true,
1893
+ },
1894
+ "error-page": {
1895
+ slug: "error-page",
1896
+ title: "Error Page",
1897
+ description: "Minimal full-page error display (404/500) with code, title, and back button.",
1898
+ category: "Feedback",
1899
+ tags: ["error", "404", "500", "not-found", "page"],
1900
+ version: "1.0.0",
1901
+ updatedAt: "2026-05-17",
1902
+ changelog: ["Initial release"],
1903
+ status: "free",
1904
+ difficulty: "beginner",
1905
+ frameworks: ["nextjs", "react", "vite"],
1906
+ dependencies: [],
1907
+ files: [{ path: "components/blocks/error-page.tsx", type: "block" }],
1908
+ tokens: ["foreground", "muted-foreground"],
1909
+ supportsDarkMode: true,
1910
+ supportsLightMode: false,
1911
+ promptReady: true,
1912
+ },
1913
+ "cookie-consent": {
1914
+ slug: "cookie-consent",
1915
+ title: "Cookie Consent",
1916
+ description: "GDPR-compliant fixed-bottom cookie consent banner with accept/decline/customize actions.",
1917
+ category: "UI",
1918
+ tags: ["cookie", "gdpr", "consent", "privacy", "banner"],
1919
+ version: "1.0.0",
1920
+ updatedAt: "2026-05-17",
1921
+ changelog: ["Initial release"],
1922
+ status: "free",
1923
+ difficulty: "beginner",
1924
+ frameworks: ["nextjs", "react", "vite"],
1925
+ dependencies: [],
1926
+ files: [{ path: "components/blocks/cookie-consent.tsx", type: "block" }],
1927
+ tokens: ["foreground", "muted-foreground", "border", "surface-2"],
1928
+ supportsDarkMode: true,
1929
+ supportsLightMode: false,
1930
+ promptReady: true,
1931
+ },
1665
1932
  };
1666
1933
  exports.blockList = Object.values(exports.manifests);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stampui/blocks",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "StampUI blocks, registry, and source files",
5
5
  "files": ["dist", "src"],
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,103 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { X } from "lucide-react"
5
+ import { cx } from "@/lib/cx"
6
+
7
+ export interface AnnouncementCTA {
8
+ label: string
9
+ href?: string
10
+ onClick?: () => void
11
+ }
12
+
13
+ export interface AnnouncementBarProps {
14
+ message: string
15
+ cta?: AnnouncementCTA
16
+ variant?: "info" | "warning" | "success"
17
+ dismissible?: boolean
18
+ className?: string
19
+ }
20
+
21
+ const variantStyles: Record<
22
+ NonNullable<AnnouncementBarProps["variant"]>,
23
+ { bar: string; label: string }
24
+ > = {
25
+ info: {
26
+ bar: "bg-[#09090B] border-b border-[#23252A]",
27
+ label: "text-muted-foreground",
28
+ },
29
+ warning: {
30
+ bar: "bg-[#09090B] border-b border-[#23252A]",
31
+ label: "text-[#FAFAFA] font-medium",
32
+ },
33
+ success: {
34
+ bar: "bg-[#09090B] border-b border-[#23252A]",
35
+ label: "text-muted-foreground",
36
+ },
37
+ }
38
+
39
+ export function AnnouncementBar({
40
+ message,
41
+ cta,
42
+ variant = "info",
43
+ dismissible = false,
44
+ className,
45
+ }: AnnouncementBarProps) {
46
+ const [dismissed, setDismissed] = React.useState(false)
47
+
48
+ if (dismissed) return null
49
+
50
+ const styles = variantStyles[variant]
51
+
52
+ return (
53
+ <div
54
+ className={cx(
55
+ "relative w-full px-4 py-2.5",
56
+ styles.bar,
57
+ className
58
+ )}
59
+ role="banner"
60
+ >
61
+ <div className="mx-auto flex max-w-5xl items-center justify-center gap-3">
62
+ <p className={cx("text-xs leading-snug", styles.label)}>
63
+ {message}
64
+ </p>
65
+
66
+ {cta && (
67
+ <>
68
+ <span className="shrink-0 text-[#23252A]" aria-hidden>
69
+ ·
70
+ </span>
71
+ {cta.href ? (
72
+ <a
73
+ href={cta.href}
74
+ className="shrink-0 text-xs font-semibold text-[#FAFAFA] underline-offset-2 hover:underline transition-colors duration-150 ease-out"
75
+ >
76
+ {cta.label}
77
+ </a>
78
+ ) : (
79
+ <button
80
+ type="button"
81
+ onClick={cta.onClick}
82
+ className="shrink-0 text-xs font-semibold text-[#FAFAFA] underline-offset-2 hover:underline transition-colors duration-150 ease-out"
83
+ >
84
+ {cta.label}
85
+ </button>
86
+ )}
87
+ </>
88
+ )}
89
+ </div>
90
+
91
+ {dismissible && (
92
+ <button
93
+ type="button"
94
+ aria-label="Dismiss announcement"
95
+ onClick={() => setDismissed(true)}
96
+ className="absolute right-3 top-1/2 -translate-y-1/2 flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors duration-150 ease-out hover:bg-[#101114] hover:text-[#FAFAFA]"
97
+ >
98
+ <X size={13} strokeWidth={2} />
99
+ </button>
100
+ )}
101
+ </div>
102
+ )
103
+ }
@@ -0,0 +1,115 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cx } from "@/lib/cx"
5
+
6
+ export interface ChangelogEntry {
7
+ version: string
8
+ date: string
9
+ title: string
10
+ description: string
11
+ tags?: string[]
12
+ type: "feature" | "fix" | "breaking"
13
+ }
14
+
15
+ export interface ChangelogFeedProps {
16
+ entries?: ChangelogEntry[]
17
+ className?: string
18
+ }
19
+
20
+ const TYPE_STYLES: Record<ChangelogEntry["type"], string> = {
21
+ feature: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
22
+ fix: "bg-sky-500/10 text-sky-400 border-sky-500/20",
23
+ breaking: "bg-red-500/10 text-red-400 border-red-500/20",
24
+ }
25
+
26
+ const TYPE_LABELS: Record<ChangelogEntry["type"], string> = {
27
+ feature: "Feature",
28
+ fix: "Fix",
29
+ breaking: "Breaking",
30
+ }
31
+
32
+ const DEFAULT_ENTRIES: ChangelogEntry[] = [
33
+ {
34
+ version: "v1.4.0",
35
+ date: "May 14, 2025",
36
+ title: "Block registry search",
37
+ description: "Full-text search across all blocks with keyboard navigation and instant previews.",
38
+ tags: ["search", "dx"],
39
+ type: "feature",
40
+ },
41
+ {
42
+ version: "v1.3.2",
43
+ date: "May 8, 2025",
44
+ title: "Fix dark mode flicker on hydration",
45
+ description: "Resolved a flash of unstyled content when the page first loads in dark mode.",
46
+ tags: ["ssr"],
47
+ type: "fix",
48
+ },
49
+ {
50
+ version: "v1.3.0",
51
+ date: "Apr 29, 2025",
52
+ title: "Stamp CLI v2",
53
+ description: "Redesigned CLI with interactive prompts, dry-run mode, and conflict detection.",
54
+ tags: ["cli"],
55
+ type: "breaking",
56
+ },
57
+ ]
58
+
59
+ export function ChangelogFeed({
60
+ entries = DEFAULT_ENTRIES,
61
+ className,
62
+ }: ChangelogFeedProps) {
63
+ return (
64
+ <section className={cx("w-full py-16 px-6", className)}>
65
+ <div className="max-w-2xl mx-auto">
66
+ <h2 className="text-2xl font-bold tracking-tight text-[#FAFAFA] mb-10">
67
+ Changelog
68
+ </h2>
69
+
70
+ <div className="relative border-l border-[#23252A] pl-8 flex flex-col gap-10">
71
+ {entries.map((entry, i) => (
72
+ <div key={i} className="relative">
73
+ <span className="absolute -left-[calc(2rem+5px)] top-1.5 w-2.5 h-2.5 rounded-full bg-[#23252A] border border-[#23252A] ring-4 ring-[#070708]" />
74
+
75
+ <div className="flex flex-wrap items-center gap-2 mb-2">
76
+ <span className="text-xs font-mono font-medium text-[#FAFAFA] bg-[#101114] border border-[#23252A] px-2 py-0.5 rounded-md">
77
+ {entry.version}
78
+ </span>
79
+ <span
80
+ className={cx(
81
+ "text-xs font-medium border px-2 py-0.5 rounded-md",
82
+ TYPE_STYLES[entry.type]
83
+ )}
84
+ >
85
+ {TYPE_LABELS[entry.type]}
86
+ </span>
87
+ <span className="text-xs text-muted-foreground">{entry.date}</span>
88
+ </div>
89
+
90
+ <h3 className="text-base font-semibold text-[#FAFAFA] leading-snug">
91
+ {entry.title}
92
+ </h3>
93
+ <p className="mt-1.5 text-sm text-muted-foreground leading-relaxed">
94
+ {entry.description}
95
+ </p>
96
+
97
+ {entry.tags && entry.tags.length > 0 && (
98
+ <div className="mt-3 flex flex-wrap gap-1.5">
99
+ {entry.tags.map((tag) => (
100
+ <span
101
+ key={tag}
102
+ className="text-xs font-mono text-muted-foreground bg-[#09090B] border border-[#23252A] px-2 py-0.5 rounded-md"
103
+ >
104
+ {tag}
105
+ </span>
106
+ ))}
107
+ </div>
108
+ )}
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+ </section>
114
+ )
115
+ }
@@ -0,0 +1,73 @@
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 CookieConsentProps {
8
+ onAccept: () => void
9
+ onDecline: () => void
10
+ onCustomize?: () => void
11
+ title?: string
12
+ description?: string
13
+ className?: string
14
+ }
15
+
16
+ export function CookieConsent({
17
+ onAccept,
18
+ onDecline,
19
+ onCustomize,
20
+ title = "We use cookies",
21
+ description = "We use cookies to improve your experience, analyze site traffic, and serve personalized content. You can accept all cookies or manage your preferences.",
22
+ className,
23
+ }: CookieConsentProps) {
24
+ return (
25
+ <div
26
+ role="dialog"
27
+ aria-label="Cookie consent"
28
+ className={cx(
29
+ "fixed bottom-0 left-0 right-0 z-50",
30
+ "border-t border-[#23252A] bg-[#09090B]",
31
+ className
32
+ )}
33
+ >
34
+ <div className="mx-auto max-w-5xl px-6 py-4">
35
+ <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-6">
36
+ <div className="flex-1 min-w-0">
37
+ <p className="text-sm font-medium text-[#FAFAFA] mb-0.5">{title}</p>
38
+ <p className="text-xs text-muted-foreground leading-relaxed">{description}</p>
39
+ </div>
40
+
41
+ <div className="flex items-center gap-2 shrink-0">
42
+ {onCustomize && (
43
+ <Button
44
+ variant="ghost"
45
+ size="sm"
46
+ onClick={onCustomize}
47
+ className="text-xs"
48
+ >
49
+ Customize
50
+ </Button>
51
+ )}
52
+ <Button
53
+ variant="outline"
54
+ size="sm"
55
+ onClick={onDecline}
56
+ className="text-xs"
57
+ >
58
+ Decline
59
+ </Button>
60
+ <Button
61
+ variant="primary"
62
+ size="sm"
63
+ onClick={onAccept}
64
+ className="text-xs"
65
+ >
66
+ Accept all
67
+ </Button>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ )
73
+ }
@@ -0,0 +1,77 @@
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 CtaAction {
8
+ label: string
9
+ href?: string
10
+ onClick?: () => void
11
+ }
12
+
13
+ export interface CtaBannerProps {
14
+ headline?: string
15
+ subtext?: string
16
+ primaryCta?: CtaAction
17
+ secondaryCta?: CtaAction
18
+ className?: string
19
+ }
20
+
21
+ export function CtaBanner({
22
+ headline = "Ready to stamp your first block?",
23
+ subtext = "Copy production-ready components into your project in seconds. No lock-in, no runtime.",
24
+ primaryCta = { label: "Get Started", href: "/blocks" },
25
+ secondaryCta = { label: "Read the Docs", href: "/docs" },
26
+ className,
27
+ }: CtaBannerProps) {
28
+ return (
29
+ <section
30
+ className={cx(
31
+ "w-full border-t border-[#23252A] py-20 px-6",
32
+ "flex flex-col items-center text-center",
33
+ className
34
+ )}
35
+ >
36
+ <h2 className="text-3xl sm:text-4xl font-bold tracking-tight text-[#FAFAFA] max-w-xl leading-tight">
37
+ {headline}
38
+ </h2>
39
+
40
+ {subtext && (
41
+ <p className="mt-4 text-base text-muted-foreground max-w-md leading-relaxed">
42
+ {subtext}
43
+ </p>
44
+ )}
45
+
46
+ <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
47
+ {primaryCta && (
48
+ <Button
49
+ asChild={!!primaryCta.href}
50
+ onClick={primaryCta.onClick}
51
+ size="lg"
52
+ >
53
+ {primaryCta.href ? (
54
+ <a href={primaryCta.href}>{primaryCta.label}</a>
55
+ ) : (
56
+ <span>{primaryCta.label}</span>
57
+ )}
58
+ </Button>
59
+ )}
60
+ {secondaryCta && (
61
+ <Button
62
+ asChild={!!secondaryCta.href}
63
+ onClick={secondaryCta.onClick}
64
+ variant="outline"
65
+ size="lg"
66
+ >
67
+ {secondaryCta.href ? (
68
+ <a href={secondaryCta.href}>{secondaryCta.label}</a>
69
+ ) : (
70
+ <span>{secondaryCta.label}</span>
71
+ )}
72
+ </Button>
73
+ )}
74
+ </div>
75
+ </section>
76
+ )
77
+ }