@ugurdemirel/landcraft 0.1.3 → 0.1.4

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.
Files changed (116) hide show
  1. package/dist/index.cjs +2 -2
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +38 -21
  4. package/dist/index.js +2276 -1956
  5. package/dist/index.js.map +1 -1
  6. package/dist/styles.css +3 -0
  7. package/package.json +1 -1
  8. package/src/components/Blog/BlogCard.tsx +12 -0
  9. package/src/components/Blog/BlogSection.tsx +95 -0
  10. package/src/components/Blog/index.ts +8 -0
  11. package/src/components/Blog/parts.tsx +38 -0
  12. package/src/components/Blog/types.ts +42 -0
  13. package/src/components/Blog/variants/Card.tsx +62 -0
  14. package/src/components/Blog/variants/Row.tsx +41 -0
  15. package/src/components/CTA/CTA.tsx +12 -0
  16. package/src/components/CTA/index.ts +2 -0
  17. package/src/components/CTA/parts.tsx +26 -0
  18. package/src/components/CTA/types.ts +12 -0
  19. package/src/components/CTA/variants/Inverse.tsx +50 -0
  20. package/src/components/CTA/variants/Panel.tsx +60 -0
  21. package/src/components/CTA/variants/Surface.tsx +34 -0
  22. package/src/components/FAQ/FAQ.tsx +48 -0
  23. package/src/components/FAQ/index.ts +2 -0
  24. package/src/components/FAQ/parts.tsx +74 -0
  25. package/src/components/FAQ/types.ts +18 -0
  26. package/src/components/FAQ/variants/Accordion.tsx +46 -0
  27. package/src/components/FAQ/variants/Cards.tsx +40 -0
  28. package/src/components/FAQ/variants/Split.tsx +44 -0
  29. package/src/components/Features/FeatureCard.tsx +44 -0
  30. package/src/components/Features/FeatureGrid.tsx +14 -0
  31. package/src/components/Features/index.ts +3 -0
  32. package/src/components/Features/parts.ts +15 -0
  33. package/src/components/Features/types.ts +25 -0
  34. package/src/components/Features/variants/Bento.tsx +32 -0
  35. package/src/components/Features/variants/Columns.tsx +21 -0
  36. package/src/components/Features/variants/EditorialRows.tsx +38 -0
  37. package/src/components/Footer/Footer.tsx +11 -0
  38. package/src/components/Footer/index.ts +2 -0
  39. package/src/components/Footer/parts.tsx +101 -0
  40. package/src/components/Footer/types.ts +32 -0
  41. package/src/components/Footer/variants/Classic.tsx +59 -0
  42. package/src/components/Footer/variants/Editorial.tsx +47 -0
  43. package/src/components/Footer/variants/Minimal.tsx +33 -0
  44. package/src/components/Hero/Hero.tsx +12 -0
  45. package/src/components/Hero/index.ts +2 -0
  46. package/src/components/Hero/parts.tsx +29 -0
  47. package/src/components/Hero/types.ts +20 -0
  48. package/src/components/Hero/variants/Centered.tsx +54 -0
  49. package/src/components/Hero/variants/Split.tsx +53 -0
  50. package/src/components/Hero/variants/Statement.tsx +60 -0
  51. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +37 -0
  52. package/src/components/LanguageSwitcher/index.ts +6 -0
  53. package/src/components/LanguageSwitcher/parts.tsx +144 -0
  54. package/src/components/LanguageSwitcher/types.ts +40 -0
  55. package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +23 -0
  56. package/src/components/LanguageSwitcher/variants/Dropdown.tsx +88 -0
  57. package/src/components/LanguageSwitcher/variants/Modal.tsx +56 -0
  58. package/src/components/LogoCloud/LogoCloud.tsx +14 -0
  59. package/src/components/LogoCloud/index.ts +2 -0
  60. package/src/components/LogoCloud/parts.tsx +32 -0
  61. package/src/components/LogoCloud/types.ts +16 -0
  62. package/src/components/LogoCloud/variants/Marquee.tsx +30 -0
  63. package/src/components/LogoCloud/variants/Quiet.tsx +26 -0
  64. package/src/components/LogoCloud/variants/Strip.tsx +25 -0
  65. package/src/components/{MegaMenu.tsx → MegaMenu/MegaMenu.tsx} +100 -127
  66. package/src/components/MegaMenu/index.ts +9 -0
  67. package/src/components/MegaMenu/types.ts +56 -0
  68. package/src/components/Modal.tsx +1 -0
  69. package/src/components/Navbar/Navbar.tsx +185 -0
  70. package/src/components/Navbar/index.ts +2 -0
  71. package/src/components/Navbar/types.ts +30 -0
  72. package/src/components/Newsletter/Newsletter.tsx +12 -0
  73. package/src/components/Newsletter/index.ts +2 -0
  74. package/src/components/Newsletter/parts.tsx +23 -0
  75. package/src/components/Newsletter/types.ts +12 -0
  76. package/src/components/Newsletter/useNewsletterForm.ts +27 -0
  77. package/src/components/Newsletter/variants/Card.tsx +62 -0
  78. package/src/components/Newsletter/variants/Inline.tsx +54 -0
  79. package/src/components/Newsletter/variants/Underline.tsx +52 -0
  80. package/src/components/Pricing/Pricing.tsx +37 -0
  81. package/src/components/Pricing/index.ts +2 -0
  82. package/src/components/Pricing/parts.tsx +132 -0
  83. package/src/components/Pricing/types.ts +28 -0
  84. package/src/components/Pricing/variants/Bento.tsx +72 -0
  85. package/src/components/Pricing/variants/Cards.tsx +79 -0
  86. package/src/components/Pricing/variants/Compact.tsx +69 -0
  87. package/src/components/Stats/Stats.tsx +16 -0
  88. package/src/components/Stats/index.ts +2 -0
  89. package/src/components/Stats/parts.tsx +29 -0
  90. package/src/components/Stats/types.ts +23 -0
  91. package/src/components/Stats/variants/Cells.tsx +34 -0
  92. package/src/components/Stats/variants/Editorial.tsx +41 -0
  93. package/src/components/Stats/variants/Hairline.tsx +31 -0
  94. package/src/components/Stats/variants/Ticker.tsx +36 -0
  95. package/src/components/Testimonials/TestimonialCard.tsx +61 -0
  96. package/src/components/Testimonials/Testimonials.tsx +14 -0
  97. package/src/components/Testimonials/index.ts +3 -0
  98. package/src/components/Testimonials/parts.ts +7 -0
  99. package/src/components/Testimonials/types.ts +24 -0
  100. package/src/components/Testimonials/variants/Carousel.tsx +94 -0
  101. package/src/components/Testimonials/variants/Grid.tsx +19 -0
  102. package/src/components/Testimonials/variants/Marquee.tsx +28 -0
  103. package/src/utils/useTokenForeground.ts +1 -0
  104. package/src/components/Blog.tsx +0 -251
  105. package/src/components/CTA.tsx +0 -155
  106. package/src/components/FAQ.tsx +0 -163
  107. package/src/components/Features.tsx +0 -153
  108. package/src/components/Footer.tsx +0 -185
  109. package/src/components/Hero.tsx +0 -169
  110. package/src/components/LanguageSwitcher.tsx +0 -223
  111. package/src/components/LogoCloud.tsx +0 -100
  112. package/src/components/Navbar.tsx +0 -195
  113. package/src/components/Newsletter.tsx +0 -180
  114. package/src/components/Pricing.tsx +0 -336
  115. package/src/components/Stats.tsx +0 -181
  116. package/src/components/Testimonials.tsx +0 -205
@@ -0,0 +1,19 @@
1
+ import { forwardRef, type HTMLAttributes } from "react";
2
+ import { cn } from "../../../utils/cn";
3
+ import { TestimonialCard } from "../TestimonialCard";
4
+ import type { Testimonial } from "../types";
5
+
6
+ interface GridProps extends HTMLAttributes<HTMLDivElement> {
7
+ items: Testimonial[];
8
+ }
9
+
10
+ export const TestimonialsGrid = forwardRef<HTMLDivElement, GridProps>(
11
+ ({ className, items, ...props }, ref) => (
12
+ <div className={cn("columns-1 gap-4 md:columns-2 lg:columns-3", className)} ref={ref} {...props}>
13
+ {items.map((item, i) => (
14
+ <TestimonialCard key={i} testimonial={item} />
15
+ ))}
16
+ </div>
17
+ ),
18
+ );
19
+ TestimonialsGrid.displayName = "TestimonialsGrid";
@@ -0,0 +1,28 @@
1
+ import { forwardRef, type HTMLAttributes } from "react";
2
+ import { cn } from "../../../utils/cn";
3
+ import { TestimonialCard } from "../TestimonialCard";
4
+ import type { Testimonial } from "../types";
5
+
6
+ interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
7
+ items: Testimonial[];
8
+ }
9
+
10
+ export const TestimonialsMarquee = forwardRef<HTMLDivElement, MarqueeProps>(
11
+ ({ className, items, ...props }, ref) => {
12
+ const doubled = [...items, ...items];
13
+ return (
14
+ <div className={cn("w-full overflow-hidden", className)} ref={ref} {...props}>
15
+ <div className="mask-fade-x">
16
+ <div className="flex w-max animate-marquee gap-4 pr-4 motion-reduce:animate-none">
17
+ {doubled.map((item, i) => (
18
+ <div key={i} className="w-[19rem] shrink-0 sm:w-[24rem]">
19
+ <TestimonialCard testimonial={item} className="m-0" />
20
+ </div>
21
+ ))}
22
+ </div>
23
+ </div>
24
+ </div>
25
+ );
26
+ },
27
+ );
28
+ TestimonialsMarquee.displayName = "TestimonialsMarquee";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { useEffect, useState } from "react";
2
3
  import { getContrastText } from "./contrast";
3
4
 
@@ -1,251 +0,0 @@
1
- import { forwardRef, type ElementType, type HTMLAttributes, type ReactNode } from "react";
2
- import { cn } from "../utils/cn";
3
- import { ArrowRight, ArrowUpRight } from "../icons";
4
- import { Badge } from "./Badge";
5
- import { Container } from "./Container";
6
-
7
- export interface BlogAuthor {
8
- name: string;
9
- avatar?: ReactNode;
10
- }
11
-
12
- export interface BlogPost {
13
- title: string;
14
- excerpt?: string;
15
- /** Display date, e.g. "12 Aug 2026". */
16
- date: string;
17
- readTime?: string;
18
- category?: string;
19
- cover?: string;
20
- /** Arbitrary CSS color for the cover placeholder accent. */
21
- accent?: string;
22
- href?: string;
23
- author?: BlogAuthor;
24
- }
25
-
26
- function AuthorMark({ name, avatar }: { name: string; avatar?: ReactNode }) {
27
- if (avatar) return avatar;
28
- const initials = name
29
- .split(" ")
30
- .map((p) => p[0])
31
- .join("")
32
- .slice(0, 2)
33
- .toUpperCase();
34
- return (
35
- <span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-primary-soft text-[11px] font-bold text-primary">
36
- {initials}
37
- </span>
38
- );
39
- }
40
-
41
- export interface BlogCardProps extends HTMLAttributes<HTMLAnchorElement> {
42
- post: BlogPost;
43
- option?: "card" | "row";
44
- /** Component used to render the card link. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
45
- LinkComponent?: ElementType;
46
- }
47
-
48
- const Cover = ({ post }: { post: BlogPost }) =>
49
- post.cover ? (
50
- <img src={post.cover} alt="" loading="lazy" className="aspect-[16/10] w-full object-cover" />
51
- ) : (
52
- <div
53
- className="grid aspect-[16/10] w-full place-items-center bg-surface-strong"
54
- style={
55
- post.accent
56
- ? { background: `linear-gradient(135deg, ${post.accent}22, ${post.accent}11)` }
57
- : undefined
58
- }
59
- >
60
- <span
61
- className="font-display text-4xl font-bold tracking-[-0.04em]"
62
- style={{ color: post.accent ?? "rgb(var(--color-foreground) / 0.12)" }}
63
- >
64
- {post.category?.[0]?.toUpperCase()}
65
- </span>
66
- </div>
67
- );
68
-
69
- export const BlogCard = forwardRef<HTMLAnchorElement, BlogCardProps>(
70
- ({ className, post, option = "card", LinkComponent, ...props }, ref) => {
71
- const Link = LinkComponent ?? "a";
72
-
73
- if (option === "row") {
74
- return (
75
- <Link
76
- ref={ref}
77
- href={post.href ?? "#"}
78
- className={cn("group flex cursor-pointer items-baseline gap-5 py-6", className)}
79
- {...props}
80
- >
81
- <div className="min-w-0 flex-1">
82
- <div className="flex items-center gap-3">
83
- {post.category ? <Badge variant="soft" size="sm">{post.category}</Badge> : null}
84
- <span className="text-xs tabular-nums text-muted-foreground">
85
- {post.date}
86
- {post.readTime ? ` · ${post.readTime}` : ""}
87
- </span>
88
- </div>
89
- <h3 className="mt-2.5 font-display text-xl font-semibold tracking-tight text-foreground transition-transform duration-200 group-hover:translate-x-1 sm:text-2xl">
90
- {post.title}
91
- </h3>
92
- </div>
93
- <span className="text-foreground/30 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-foreground">
94
- <ArrowUpRight className="h-5 w-5" />
95
- </span>
96
- </Link>
97
- );
98
- }
99
-
100
- const author = post.author;
101
- return (
102
- <Link
103
- ref={ref}
104
- href={post.href ?? "#"}
105
- className={cn(
106
- "group flex h-full cursor-pointer flex-col overflow-hidden rounded-xl border border-border bg-surface transition-colors duration-200 hover:border-foreground/20",
107
- className,
108
- )}
109
- {...props}
110
- >
111
- <div className="overflow-hidden">
112
- <Cover post={post} />
113
- </div>
114
- <div className="flex flex-1 flex-col p-6">
115
- <div className="flex items-center justify-between gap-3">
116
- {post.category ? <Badge variant="soft" size="sm">{post.category}</Badge> : <span />}
117
- {post.readTime ? (
118
- <span className="text-xs text-muted-foreground">{post.readTime}</span>
119
- ) : null}
120
- </div>
121
- <h3 className="mt-4 font-display text-lg font-semibold leading-snug tracking-tight text-foreground sm:text-xl">
122
- {post.title}
123
- </h3>
124
- {post.excerpt ? (
125
- <p className="mt-2.5 line-clamp-2 text-sm leading-6 text-muted-foreground">
126
- {post.excerpt}
127
- </p>
128
- ) : null}
129
- <div className="mt-5 flex items-center justify-between gap-3 border-t border-border pt-4">
130
- {author ? (
131
- <span className="flex min-w-0 items-center gap-2.5">
132
- <AuthorMark name={author.name} avatar={author.avatar} />
133
- <span className="truncate text-xs font-medium text-foreground">{author.name}</span>
134
- </span>
135
- ) : (
136
- <span className="text-xs tabular-nums text-muted-foreground">{post.date}</span>
137
- )}
138
- <ArrowRight className="h-4 w-4 text-foreground/35 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-foreground" />
139
- </div>
140
- </div>
141
- </Link>
142
- );
143
- },
144
- );
145
- BlogCard.displayName = "BlogCard";
146
-
147
- export interface BlogSectionProps
148
- extends Omit<HTMLAttributes<HTMLElement>, "title"> {
149
- posts: BlogPost[];
150
- /** Show only the first N posts. */
151
- limit?: number;
152
- columns?: 2 | 3;
153
- option?: "card" | "row";
154
- eyebrow?: ReactNode;
155
- eyebrowStyle?: "caps" | "soft" | "plain";
156
- title?: ReactNode;
157
- description?: ReactNode;
158
- showAllLabel?: string;
159
- showAllHref?: string;
160
- /** Component used to render the "show all" link and post cards. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
161
- LinkComponent?: ElementType;
162
- }
163
-
164
- const gridCols: Record<NonNullable<BlogSectionProps["columns"]>, string> = {
165
- 2: "sm:grid-cols-2",
166
- 3: "sm:grid-cols-2 lg:grid-cols-3",
167
- };
168
-
169
- export const BlogSection = forwardRef<HTMLElement, BlogSectionProps>(
170
- (
171
- {
172
- className,
173
- posts,
174
- limit,
175
- columns = 3,
176
- option = "card",
177
- eyebrow,
178
- eyebrowStyle = "caps",
179
- title,
180
- description,
181
- showAllLabel,
182
- showAllHref,
183
- LinkComponent,
184
- ...props
185
- },
186
- ref,
187
- ) => {
188
- const visible = limit ? posts.slice(0, limit) : posts;
189
- const Link = LinkComponent ?? "a";
190
-
191
- return (
192
- <section ref={ref} className={cn("w-full py-20 sm:py-24", className)} {...props}>
193
- <Container>
194
- {(eyebrow || title || description || showAllHref) && (
195
- <div className="mb-12 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
196
- <div className="max-w-2xl">
197
- {eyebrow ? (
198
- <p
199
- className={cn(
200
- "text-xs font-semibold uppercase tracking-[0.18em]",
201
- eyebrowStyle === "caps" && "text-foreground/50",
202
- eyebrowStyle === "soft" &&
203
- "inline-flex items-center rounded-full bg-primary-soft px-3 py-1.5 text-primary",
204
- eyebrowStyle === "plain" && "text-primary",
205
- )}
206
- >
207
- {eyebrow}
208
- </p>
209
- ) : null}
210
- {title ? (
211
- <h2 className="mt-4 font-display text-balance text-3xl font-semibold leading-[1.08] tracking-tight text-foreground sm:text-4xl">
212
- {title}
213
- </h2>
214
- ) : null}
215
- {description ? (
216
- <p className="mt-4 text-pretty text-base leading-7 text-muted-foreground">
217
- {description}
218
- </p>
219
- ) : null}
220
- </div>
221
- {showAllHref ? (
222
- <Link
223
- href={showAllHref}
224
- className="group inline-flex shrink-0 cursor-pointer items-center gap-1.5 text-sm font-semibold text-foreground transition-colors duration-150 hover:text-primary"
225
- >
226
- {showAllLabel ?? "All posts"}
227
- <ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
228
- </Link>
229
- ) : null}
230
- </div>
231
- )}
232
-
233
- {option === "row" ? (
234
- <div className="divide-y divide-border border-t border-border">
235
- {visible.map((post) => (
236
- <BlogCard key={post.title} post={post} option="row" LinkComponent={LinkComponent} />
237
- ))}
238
- </div>
239
- ) : (
240
- <div className={cn("grid grid-cols-1 gap-4", gridCols[columns])}>
241
- {visible.map((post) => (
242
- <BlogCard key={post.title} post={post} option="card" LinkComponent={LinkComponent} />
243
- ))}
244
- </div>
245
- )}
246
- </Container>
247
- </section>
248
- );
249
- },
250
- );
251
- BlogSection.displayName = "BlogSection";
@@ -1,155 +0,0 @@
1
- import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
- import { cn } from "../utils/cn";
3
- import { useTokenForeground } from "../utils/useTokenForeground";
4
- import { Container } from "./Container";
5
-
6
- export interface CTAProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
7
- title: ReactNode;
8
- description?: ReactNode;
9
- action?: ReactNode;
10
- secondaryAction?: ReactNode;
11
- option?: "panel" | "surface" | "inverse";
12
- align?: "left" | "center";
13
- }
14
-
15
- export const CTA = forwardRef<HTMLElement, CTAProps>(
16
- (
17
- { className, title, description, action, secondaryAction, option = "panel", align = "left", id, ...props },
18
- ref,
19
- ) => {
20
- const isPanel = option === "panel";
21
- const isInverse = option === "inverse";
22
- // panel → brand gradient; inverse → ink. Readable text must be derived from
23
- // the actual painting token so contrast survives any palette.
24
- const fg = useTokenForeground(isInverse ? "--color-secondary" : "--color-primary");
25
-
26
- /** surface — quiet paper band: hairline rule, big display type, actions right. */
27
- if (option === "surface") {
28
- return (
29
- <section ref={ref} id={id} className={cn("w-full", className)} {...props}>
30
- <Container className="py-20 sm:py-24">
31
- <div
32
- className={cn(
33
- "flex flex-col gap-10 border-t border-border pt-14 sm:flex-row sm:items-end sm:justify-between sm:pt-16",
34
- align === "center" && "items-center text-center sm:flex-col sm:items-center",
35
- )}
36
- >
37
- <div className={cn("max-w-3xl", align === "center" && "mx-auto")}>
38
- <h2 className="font-display text-4xl font-semibold leading-[1.03] tracking-[-0.03em] text-balance text-foreground sm:text-6xl">
39
- {title}
40
- </h2>
41
- {description ? (
42
- <p className={cn("mt-5 max-w-xl text-pretty text-base leading-7 text-muted-foreground sm:text-lg", align === "center" && "mx-auto")}>
43
- {description}
44
- </p>
45
- ) : null}
46
- </div>
47
- <div
48
- className={cn(
49
- "flex flex-wrap items-center gap-4",
50
- align === "center" && "justify-center",
51
- )}
52
- >
53
- {action}
54
- {secondaryAction}
55
- </div>
56
- </div>
57
- </Container>
58
- </section>
59
- );
60
- }
61
-
62
- return (
63
- <section ref={ref} id={id} className={cn("w-full py-6 pb-20", className)} {...props}>
64
- <Container>
65
- {isPanel ? (
66
- <div
67
- className="relative overflow-hidden rounded-[1.75rem] px-7 py-14 sm:px-14 sm:py-20"
68
- style={{
69
- background:
70
- "radial-gradient(120% 160% at 0% 0%, rgb(var(--color-primary)), rgb(var(--color-primary-hover)) 55%, rgb(var(--color-primary) / 0.85))",
71
- color: fg,
72
- }}
73
- >
74
- <div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.07] mix-blend-overlay" />
75
- <div
76
- aria-hidden
77
- className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full"
78
- style={{ background: "rgb(var(--color-accent))", opacity: 0.35, filter: "blur(60px)" }}
79
- />
80
- <div
81
- className={cn(
82
- "relative flex flex-col gap-9",
83
- align === "center" ? "items-center text-center" : "items-start",
84
- )}
85
- >
86
- <div className={cn("max-w-2xl", align === "center" && "mx-auto")}>
87
- <h2 className="font-display text-balance text-3xl font-semibold leading-[1.06] tracking-[-0.02em] sm:text-5xl">
88
- {title}
89
- </h2>
90
- {description ? (
91
- <p
92
- className={cn(
93
- "mt-4 text-pretty text-base leading-7 sm:text-lg",
94
- align === "center" && "mx-auto max-w-xl",
95
- )}
96
- >
97
- {description}
98
- </p>
99
- ) : null}
100
- </div>
101
- <div
102
- className={cn(
103
- "flex flex-wrap items-center gap-4",
104
- align === "center" && "justify-center",
105
- )}
106
- >
107
- {action}
108
- {secondaryAction}
109
- </div>
110
- </div>
111
- </div>
112
- ) : (
113
- <div
114
- className="relative overflow-hidden rounded-[1.75rem] px-7 py-16 sm:px-16 sm:py-20"
115
- style={{
116
- backgroundColor: "rgb(var(--color-secondary))",
117
- color: fg,
118
- }}
119
- >
120
- <div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.09] mix-blend-overlay" />
121
- <div
122
- className={cn(
123
- "relative flex flex-col items-start gap-10",
124
- align === "center" && "items-center text-center",
125
- "lg:flex-row lg:items-end lg:justify-between",
126
- )}
127
- >
128
- <div className={cn("max-w-3xl", align === "center" && "mx-auto")}>
129
- <h2 className="font-display text-balance text-4xl font-semibold leading-[1.02] tracking-[-0.03em] sm:text-6xl">
130
- {title}
131
- </h2>
132
- {description ? (
133
- <p className={cn("mt-5 max-w-xl text-pretty text-base leading-7 sm:text-lg", align === "center" && "mx-auto")}>
134
- {description}
135
- </p>
136
- ) : null}
137
- </div>
138
- <div
139
- className={cn(
140
- "flex flex-wrap items-center gap-4",
141
- align === "center" && "justify-center",
142
- )}
143
- >
144
- {action}
145
- {secondaryAction}
146
- </div>
147
- </div>
148
- </div>
149
- )}
150
- </Container>
151
- </section>
152
- );
153
- },
154
- );
155
- CTA.displayName = "CTA";
@@ -1,163 +0,0 @@
1
- import { useState, type HTMLAttributes, type ReactNode } from "react";
2
- import { cn } from "../utils/cn";
3
- import { Plus } from "../icons";
4
-
5
- export interface FaqItem {
6
- question: string;
7
- answer: string;
8
- }
9
-
10
- export interface FAQProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
11
- items: FaqItem[];
12
- option?: "accordion" | "split" | "cards";
13
- allowMultiple?: boolean;
14
- defaultOpen?: number[];
15
- eyebrow?: ReactNode;
16
- title?: ReactNode;
17
- description?: ReactNode;
18
- }
19
-
20
- export const FAQ = ({
21
- className,
22
- items,
23
- option = "accordion",
24
- allowMultiple = false,
25
- defaultOpen = [],
26
- eyebrow,
27
- title,
28
- description,
29
- ...props
30
- }: FAQProps) => {
31
- const [openSet, setOpenSet] = useState<Set<number>>(
32
- () => new Set(allowMultiple ? defaultOpen : defaultOpen.slice(0, 1)),
33
- );
34
-
35
- const toggle = (index: number) => {
36
- setOpenSet((prev) => {
37
- const next = new Set(prev);
38
- if (next.has(index)) {
39
- next.delete(index);
40
- } else {
41
- if (!allowMultiple) next.clear();
42
- next.add(index);
43
- }
44
- return next;
45
- });
46
- };
47
-
48
- const headerRow = (label: string) => (
49
- <p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{label}</p>
50
- );
51
-
52
- /** cards — always-open pairs in a quiet hairline grid. */
53
- if (option === "cards") {
54
- return (
55
- <div className={cn("w-full", className)} {...props}>
56
- {eyebrow ? <div className="mb-10">{headerRow(String(eyebrow))}</div> : null}
57
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
58
- {items.map((item, i) => (
59
- <div
60
- key={item.question}
61
- className="flex flex-col rounded-xl border border-border bg-surface p-7"
62
- >
63
- <div className="flex items-baseline gap-4">
64
- <span className="font-display text-sm font-semibold tabular-nums text-foreground/35">
65
- {String(i + 1).padStart(2, "0")}
66
- </span>
67
- <h3 className="font-display text-lg font-semibold tracking-tight text-foreground">
68
- {item.question}
69
- </h3>
70
- </div>
71
- <p className="mt-4 border-t border-border pt-4 text-sm leading-6 text-muted-foreground">
72
- {item.answer}
73
- </p>
74
- </div>
75
- ))}
76
- </div>
77
- </div>
78
- );
79
- }
80
-
81
- /** accordion — single-column, hairline divided. */
82
- const accordion = (
83
- <div className="divide-y divide-border rounded-2xl border border-border bg-surface">
84
- {items.map((item, index) => {
85
- const open = openSet.has(index);
86
- return (
87
- <div key={item.question}>
88
- <h3>
89
- <button
90
- type="button"
91
- aria-expanded={open}
92
- onClick={() => toggle(index)}
93
- className="flex w-full cursor-pointer items-center justify-between gap-4 px-6 py-5 text-left text-[15px] font-medium text-foreground transition-colors duration-150 hover:bg-surface-strong/60 focus-visible:outline-none"
94
- >
95
- {item.question}
96
- <Plus
97
- className={cn(
98
- "h-4.5 w-4.5 shrink-0 text-foreground/45 transition-transform duration-200 sm:h-5 sm:w-5",
99
- open && "rotate-45 text-primary",
100
- )}
101
- />
102
- </button>
103
- </h3>
104
- {open ? (
105
- <p className="px-6 pb-6 text-sm leading-7 text-muted-foreground">{item.answer}</p>
106
- ) : null}
107
- </div>
108
- );
109
- })}
110
- </div>
111
- );
112
-
113
- if (option === "split") {
114
- return (
115
- <div className={cn("grid w-full gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:gap-20", className)} {...props}>
116
- <div className="lg:sticky lg:top-24 lg:self-start">
117
- {eyebrow ? headerRow(String(eyebrow)) : null}
118
- {title ? (
119
- <h2 className="mt-4 font-display text-3xl font-semibold leading-[1.08] tracking-tight text-foreground sm:text-4xl">
120
- {title}
121
- </h2>
122
- ) : null}
123
- {description ? (
124
- <p className="mt-4 text-pretty text-base leading-7 text-muted-foreground">{description}</p>
125
- ) : null}
126
- </div>
127
- <div className="divide-y divide-border">
128
- {items.map((item, index) => {
129
- const open = openSet.has(index);
130
- return (
131
- <div key={item.question}>
132
- <h3>
133
- <button
134
- type="button"
135
- aria-expanded={open}
136
- onClick={() => toggle(index)}
137
- className="flex w-full cursor-pointer items-center justify-between gap-4 py-5 text-left text-base font-semibold text-foreground transition-colors duration-150 focus-visible:outline-none"
138
- >
139
- {item.question}
140
- <Plus
141
- className={cn(
142
- "h-4.5 w-4.5 shrink-0 text-foreground/45 transition-transform duration-200 sm:h-5 sm:w-5",
143
- open && "rotate-45 text-primary",
144
- )}
145
- />
146
- </button>
147
- </h3>
148
- {open ? <p className="pb-6 text-sm leading-7 text-muted-foreground">{item.answer}</p> : null}
149
- </div>
150
- );
151
- })}
152
- </div>
153
- </div>
154
- );
155
- }
156
-
157
- return (
158
- <div className={cn("mx-auto w-full max-w-3xl", className)} {...props}>
159
- {accordion}
160
- </div>
161
- );
162
- };
163
- FAQ.displayName = "FAQ";