@ugurdemirel/landcraft 0.1.2 → 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 +41 -22
  4. package/dist/index.js +2261 -1937
  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} +108 -126
  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
package/dist/styles.css CHANGED
@@ -2071,6 +2071,9 @@
2071
2071
  .overflow-y-auto {
2072
2072
  overflow-y: auto;
2073
2073
  }
2074
+ .rounded {
2075
+ border-radius: 0.25rem;
2076
+ }
2074
2077
  .rounded-2xl {
2075
2078
  border-radius: var(--radius-2xl);
2076
2079
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ugurdemirel/landcraft",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -0,0 +1,12 @@
1
+ import { forwardRef } from "react";
2
+ import type { BlogCardProps } from "./types";
3
+ import { BlogCardCard } from "./variants/Card";
4
+ import { BlogCardRow } from "./variants/Row";
5
+
6
+ export const BlogCard = forwardRef<HTMLAnchorElement, BlogCardProps>(
7
+ ({ option = "card", ...props }, ref) => {
8
+ if (option === "row") return <BlogCardRow ref={ref} {...props} />;
9
+ return <BlogCardCard ref={ref} {...props} />;
10
+ },
11
+ );
12
+ BlogCard.displayName = "BlogCard";
@@ -0,0 +1,95 @@
1
+ import { forwardRef } from "react";
2
+ import { cn } from "../../utils/cn";
3
+ import { ArrowRight } from "../../icons";
4
+ import { Container } from "../Container";
5
+ import { BlogCard } from "./BlogCard";
6
+ import type { BlogSectionProps } from "./types";
7
+
8
+ const gridCols: Record<2 | 3, string> = {
9
+ 2: "sm:grid-cols-2",
10
+ 3: "sm:grid-cols-2 lg:grid-cols-3",
11
+ };
12
+
13
+ export const BlogSection = forwardRef<HTMLElement, BlogSectionProps>(
14
+ (
15
+ {
16
+ className,
17
+ posts,
18
+ limit,
19
+ columns = 3,
20
+ option = "card",
21
+ eyebrow,
22
+ eyebrowStyle = "caps",
23
+ title,
24
+ description,
25
+ showAllLabel,
26
+ showAllHref,
27
+ LinkComponent,
28
+ ...props
29
+ },
30
+ ref,
31
+ ) => {
32
+ const visible = limit ? posts.slice(0, limit) : posts;
33
+ const Link = LinkComponent ?? "a";
34
+
35
+ return (
36
+ <section ref={ref} className={cn("w-full py-20 sm:py-24", className)} {...props}>
37
+ <Container>
38
+ {(eyebrow || title || description || showAllHref) && (
39
+ <div className="mb-12 flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
40
+ <div className="max-w-2xl">
41
+ {eyebrow ? (
42
+ <p
43
+ className={cn(
44
+ "text-xs font-semibold uppercase tracking-[0.18em]",
45
+ eyebrowStyle === "caps" && "text-foreground/50",
46
+ eyebrowStyle === "soft" &&
47
+ "inline-flex items-center rounded-full bg-primary-soft px-3 py-1.5 text-primary",
48
+ eyebrowStyle === "plain" && "text-primary",
49
+ )}
50
+ >
51
+ {eyebrow}
52
+ </p>
53
+ ) : null}
54
+ {title ? (
55
+ <h2 className="mt-4 font-display text-balance text-3xl font-semibold leading-[1.08] tracking-tight text-foreground sm:text-4xl">
56
+ {title}
57
+ </h2>
58
+ ) : null}
59
+ {description ? (
60
+ <p className="mt-4 text-pretty text-base leading-7 text-muted-foreground">
61
+ {description}
62
+ </p>
63
+ ) : null}
64
+ </div>
65
+ {showAllHref ? (
66
+ <Link
67
+ href={showAllHref}
68
+ 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"
69
+ >
70
+ {showAllLabel ?? "All posts"}
71
+ <ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
72
+ </Link>
73
+ ) : null}
74
+ </div>
75
+ )}
76
+
77
+ {option === "row" ? (
78
+ <div className="divide-y divide-border border-t border-border">
79
+ {visible.map((post) => (
80
+ <BlogCard key={post.title} post={post} option="row" LinkComponent={LinkComponent} />
81
+ ))}
82
+ </div>
83
+ ) : (
84
+ <div className={cn("grid grid-cols-1 gap-4", gridCols[columns])}>
85
+ {visible.map((post) => (
86
+ <BlogCard key={post.title} post={post} option="card" LinkComponent={LinkComponent} />
87
+ ))}
88
+ </div>
89
+ )}
90
+ </Container>
91
+ </section>
92
+ );
93
+ },
94
+ );
95
+ BlogSection.displayName = "BlogSection";
@@ -0,0 +1,8 @@
1
+ export { BlogCard } from "./BlogCard";
2
+ export { BlogSection } from "./BlogSection";
3
+ export type {
4
+ BlogCardProps,
5
+ BlogSectionProps,
6
+ BlogPost,
7
+ BlogAuthor,
8
+ } from "./types";
@@ -0,0 +1,38 @@
1
+ import type { ReactNode } from "react";
2
+ import type { BlogPost } from "./types";
3
+
4
+ export function AuthorMark({ name, avatar }: { name: string; avatar?: ReactNode }) {
5
+ if (avatar) return avatar;
6
+ const initials = name
7
+ .split(" ")
8
+ .map((p) => p[0])
9
+ .join("")
10
+ .slice(0, 2)
11
+ .toUpperCase();
12
+ return (
13
+ <span className="grid h-8 w-8 shrink-0 place-items-center rounded-full bg-primary-soft text-[11px] font-bold text-primary">
14
+ {initials}
15
+ </span>
16
+ );
17
+ }
18
+
19
+ export const Cover = ({ post }: { post: BlogPost }) =>
20
+ post.cover ? (
21
+ <img src={post.cover} alt="" loading="lazy" className="aspect-[16/10] w-full object-cover" />
22
+ ) : (
23
+ <div
24
+ className="grid aspect-[16/10] w-full place-items-center bg-surface-strong"
25
+ style={
26
+ post.accent
27
+ ? { background: `linear-gradient(135deg, ${post.accent}22, ${post.accent}11)` }
28
+ : undefined
29
+ }
30
+ >
31
+ <span
32
+ className="font-display text-4xl font-bold tracking-[-0.04em]"
33
+ style={{ color: post.accent ?? "rgb(var(--color-foreground) / 0.12)" }}
34
+ >
35
+ {post.category?.[0]?.toUpperCase()}
36
+ </span>
37
+ </div>
38
+ );
@@ -0,0 +1,42 @@
1
+ import type { ElementType, HTMLAttributes, ReactNode } from "react";
2
+
3
+ export interface BlogAuthor {
4
+ name: string;
5
+ avatar?: ReactNode;
6
+ }
7
+
8
+ export interface BlogPost {
9
+ title: string;
10
+ excerpt?: string;
11
+ /** Display date, e.g. "12 Aug 2026". */
12
+ date: string;
13
+ readTime?: string;
14
+ category?: string;
15
+ cover?: string;
16
+ /** Arbitrary CSS color for the cover placeholder accent. */
17
+ accent?: string;
18
+ href?: string;
19
+ author?: BlogAuthor;
20
+ }
21
+
22
+ export interface BlogCardProps extends HTMLAttributes<HTMLAnchorElement> {
23
+ post: BlogPost;
24
+ option?: "card" | "row";
25
+ /** Component used to render the card link. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
26
+ LinkComponent?: ElementType;
27
+ }
28
+
29
+ export interface BlogSectionProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
30
+ posts: BlogPost[];
31
+ /** Show only the first N posts. */
32
+ limit?: number;
33
+ columns?: 2 | 3;
34
+ option?: "card" | "row";
35
+ eyebrow?: ReactNode;
36
+ eyebrowStyle?: "caps" | "soft" | "plain";
37
+ title?: ReactNode;
38
+ description?: ReactNode;
39
+ showAllLabel?: string;
40
+ showAllHref?: string;
41
+ LinkComponent?: ElementType;
42
+ }
@@ -0,0 +1,62 @@
1
+ import { forwardRef, type ElementType } from "react";
2
+ import { cn } from "../../../utils/cn";
3
+ import { ArrowRight } from "../../../icons";
4
+ import { Badge } from "../../Badge";
5
+ import { AuthorMark, Cover } from "../parts";
6
+ import type { BlogPost } from "../types";
7
+
8
+ interface BlogCardCardProps {
9
+ post: BlogPost;
10
+ LinkComponent?: ElementType;
11
+ className?: string;
12
+ }
13
+
14
+ export const BlogCardCard = forwardRef<HTMLAnchorElement, BlogCardCardProps>(
15
+ ({ className, post, LinkComponent, ...props }, ref) => {
16
+ const Link = LinkComponent ?? "a";
17
+ const author = post.author;
18
+ return (
19
+ <Link
20
+ ref={ref}
21
+ href={post.href ?? "#"}
22
+ className={cn(
23
+ "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",
24
+ className,
25
+ )}
26
+ {...props}
27
+ >
28
+ <div className="overflow-hidden">
29
+ <Cover post={post} />
30
+ </div>
31
+ <div className="flex flex-1 flex-col p-6">
32
+ <div className="flex items-center justify-between gap-3">
33
+ {post.category ? <Badge variant="soft" size="sm">{post.category}</Badge> : <span />}
34
+ {post.readTime ? (
35
+ <span className="text-xs text-muted-foreground">{post.readTime}</span>
36
+ ) : null}
37
+ </div>
38
+ <h3 className="mt-4 font-display text-lg font-semibold leading-snug tracking-tight text-foreground sm:text-xl">
39
+ {post.title}
40
+ </h3>
41
+ {post.excerpt ? (
42
+ <p className="mt-2.5 line-clamp-2 text-sm leading-6 text-muted-foreground">
43
+ {post.excerpt}
44
+ </p>
45
+ ) : null}
46
+ <div className="mt-5 flex items-center justify-between gap-3 border-t border-border pt-4">
47
+ {author ? (
48
+ <span className="flex min-w-0 items-center gap-2.5">
49
+ <AuthorMark name={author.name} avatar={author.avatar} />
50
+ <span className="truncate text-xs font-medium text-foreground">{author.name}</span>
51
+ </span>
52
+ ) : (
53
+ <span className="text-xs tabular-nums text-muted-foreground">{post.date}</span>
54
+ )}
55
+ <ArrowRight className="h-4 w-4 text-foreground/35 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-foreground" />
56
+ </div>
57
+ </div>
58
+ </Link>
59
+ );
60
+ },
61
+ );
62
+ BlogCardCard.displayName = "BlogCardCard";
@@ -0,0 +1,41 @@
1
+ import { forwardRef, type ElementType } from "react";
2
+ import { cn } from "../../../utils/cn";
3
+ import { ArrowUpRight } from "../../../icons";
4
+ import type { BlogPost } from "../types";
5
+
6
+ interface BlogCardRowProps {
7
+ post: BlogPost;
8
+ LinkComponent?: ElementType;
9
+ className?: string;
10
+ }
11
+
12
+ export const BlogCardRow = forwardRef<HTMLAnchorElement, BlogCardRowProps>(
13
+ ({ className, post, LinkComponent, ...props }, ref) => {
14
+ const Link = LinkComponent ?? "a";
15
+ return (
16
+ <Link
17
+ ref={ref}
18
+ href={post.href ?? "#"}
19
+ className={cn("group flex cursor-pointer items-baseline gap-5 py-6", className)}
20
+ {...props}
21
+ >
22
+ <div className="min-w-0 flex-1">
23
+ <div className="flex items-center gap-3">
24
+ {post.category ? <span className="rounded-full bg-primary-soft px-2.5 py-0.5 text-xs font-semibold text-primary">{post.category}</span> : null}
25
+ <span className="text-xs tabular-nums text-muted-foreground">
26
+ {post.date}
27
+ {post.readTime ? ` · ${post.readTime}` : ""}
28
+ </span>
29
+ </div>
30
+ <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">
31
+ {post.title}
32
+ </h3>
33
+ </div>
34
+ <span className="text-foreground/30 transition-all duration-200 group-hover:translate-x-0.5 group-hover:text-foreground">
35
+ <ArrowUpRight className="h-5 w-5" />
36
+ </span>
37
+ </Link>
38
+ );
39
+ },
40
+ );
41
+ BlogCardRow.displayName = "BlogCardRow";
@@ -0,0 +1,12 @@
1
+ import { forwardRef } from "react";
2
+ import type { CTAProps } from "./types";
3
+ import { CTAPanel } from "./variants/Panel";
4
+ import { CTASurface } from "./variants/Surface";
5
+ import { CTAInverse } from "./variants/Inverse";
6
+
7
+ export const CTA = forwardRef<HTMLElement, CTAProps>(({ option = "panel", ...props }, ref) => {
8
+ if (option === "surface") return <CTASurface ref={ref} {...props} />;
9
+ if (option === "inverse") return <CTAInverse ref={ref} {...props} />;
10
+ return <CTAPanel ref={ref} {...props} />;
11
+ });
12
+ CTA.displayName = "CTA";
@@ -0,0 +1,2 @@
1
+ export { CTA } from "./CTA";
2
+ export type { CTAProps, CTAOption } from "./types";
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../../utils/cn";
3
+
4
+ export function CTAActions({
5
+ action,
6
+ secondaryAction,
7
+ align,
8
+ actionClassName = "flex flex-wrap items-center gap-4",
9
+ }: {
10
+ action?: ReactNode;
11
+ secondaryAction?: ReactNode;
12
+ align?: "left" | "center";
13
+ actionClassName?: string;
14
+ }) {
15
+ return (
16
+ <div
17
+ className={cn(
18
+ actionClassName,
19
+ align === "center" && "justify-center",
20
+ )}
21
+ >
22
+ {action}
23
+ {secondaryAction}
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,12 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ export type CTAOption = "panel" | "surface" | "inverse";
4
+
5
+ export interface CTAProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
6
+ title: ReactNode;
7
+ description?: ReactNode;
8
+ action?: ReactNode;
9
+ secondaryAction?: ReactNode;
10
+ option?: CTAOption;
11
+ align?: "left" | "center";
12
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "../../../utils/cn";
4
+ import { Container } from "../../Container";
5
+ import { useTokenForeground } from "../../../utils/useTokenForeground";
6
+ import { CTAActions } from "../parts";
7
+ import type { CTAProps } from "../types";
8
+
9
+ /** inverse — ink panel with a wide left-aligned split. */
10
+ export const CTAInverse = forwardRef<HTMLElement, CTAProps>(
11
+ ({ className, title, description, action, secondaryAction, align = "left", id, ...props }, ref) => {
12
+ const fg = useTokenForeground("--color-secondary");
13
+
14
+ return (
15
+ <section ref={ref} id={id} className={cn("w-full py-6 pb-20", className)} {...props}>
16
+ <Container>
17
+ <div
18
+ className="relative overflow-hidden rounded-[1.75rem] px-7 py-16 sm:px-16 sm:py-20"
19
+ style={{
20
+ backgroundColor: "rgb(var(--color-secondary))",
21
+ color: fg,
22
+ }}
23
+ >
24
+ <div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.09] mix-blend-overlay" />
25
+ <div
26
+ className={cn(
27
+ "relative flex flex-col items-start gap-10",
28
+ align === "center" && "items-center text-center",
29
+ "lg:flex-row lg:items-end lg:justify-between",
30
+ )}
31
+ >
32
+ <div className={cn("max-w-3xl", align === "center" && "mx-auto")}>
33
+ <h2 className="font-display text-balance text-4xl font-semibold leading-[1.02] tracking-[-0.03em] sm:text-6xl">
34
+ {title}
35
+ </h2>
36
+ {description ? (
37
+ <p className={cn("mt-5 max-w-xl text-pretty text-base leading-7 sm:text-lg", align === "center" && "mx-auto")}>
38
+ {description}
39
+ </p>
40
+ ) : null}
41
+ </div>
42
+ <CTAActions action={action} secondaryAction={secondaryAction} align={align} />
43
+ </div>
44
+ </div>
45
+ </Container>
46
+ </section>
47
+ );
48
+ },
49
+ );
50
+ CTAInverse.displayName = "CTAInverse";
@@ -0,0 +1,60 @@
1
+ "use client";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "../../../utils/cn";
4
+ import { Container } from "../../Container";
5
+ import { useTokenForeground } from "../../../utils/useTokenForeground";
6
+ import { CTAActions } from "../parts";
7
+ import type { CTAProps } from "../types";
8
+
9
+ /** panel — brand gradient panel, rounded corners. */
10
+ export const CTAPanel = forwardRef<HTMLElement, CTAProps>(
11
+ ({ className, title, description, action, secondaryAction, align = "left", id, ...props }, ref) => {
12
+ const fg = useTokenForeground("--color-primary");
13
+
14
+ return (
15
+ <section ref={ref} id={id} className={cn("w-full py-6 pb-20", className)} {...props}>
16
+ <Container>
17
+ <div
18
+ className="relative overflow-hidden rounded-[1.75rem] px-7 py-14 sm:px-14 sm:py-20"
19
+ style={{
20
+ background:
21
+ "radial-gradient(120% 160% at 0% 0%, rgb(var(--color-primary)), rgb(var(--color-primary-hover)) 55%, rgb(var(--color-primary) / 0.85))",
22
+ color: fg,
23
+ }}
24
+ >
25
+ <div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.07] mix-blend-overlay" />
26
+ <div
27
+ aria-hidden
28
+ className="pointer-events-none absolute -right-24 -top-24 h-72 w-72 rounded-full"
29
+ style={{ background: "rgb(var(--color-accent))", opacity: 0.35, filter: "blur(60px)" }}
30
+ />
31
+ <div
32
+ className={cn(
33
+ "relative flex flex-col gap-9",
34
+ align === "center" ? "items-center text-center" : "items-start",
35
+ )}
36
+ >
37
+ <div className={cn("max-w-2xl", align === "center" && "mx-auto")}>
38
+ <h2 className="font-display text-balance text-3xl font-semibold leading-[1.06] tracking-[-0.02em] sm:text-5xl">
39
+ {title}
40
+ </h2>
41
+ {description ? (
42
+ <p
43
+ className={cn(
44
+ "mt-4 text-pretty text-base leading-7 sm:text-lg",
45
+ align === "center" && "mx-auto max-w-xl",
46
+ )}
47
+ >
48
+ {description}
49
+ </p>
50
+ ) : null}
51
+ </div>
52
+ <CTAActions action={action} secondaryAction={secondaryAction} align={align} />
53
+ </div>
54
+ </div>
55
+ </Container>
56
+ </section>
57
+ );
58
+ },
59
+ );
60
+ CTAPanel.displayName = "CTAPanel";
@@ -0,0 +1,34 @@
1
+ import { forwardRef } from "react";
2
+ import { cn } from "../../../utils/cn";
3
+ import { Container } from "../../Container";
4
+ import { CTAActions } from "../parts";
5
+ import type { CTAProps } from "../types";
6
+
7
+ /** surface — quiet paper band: hairline rule, big display type, actions right. */
8
+ export const CTASurface = forwardRef<HTMLElement, CTAProps>(
9
+ ({ className, title, description, action, secondaryAction, align = "left", id, ...props }, ref) => (
10
+ <section ref={ref} id={id} className={cn("w-full", className)} {...props}>
11
+ <Container className="py-20 sm:py-24">
12
+ <div
13
+ className={cn(
14
+ "flex flex-col gap-10 border-t border-border pt-14 sm:flex-row sm:items-end sm:justify-between sm:pt-16",
15
+ align === "center" && "items-center text-center sm:flex-col sm:items-center",
16
+ )}
17
+ >
18
+ <div className={cn("max-w-3xl", align === "center" && "mx-auto")}>
19
+ <h2 className="font-display text-4xl font-semibold leading-[1.03] tracking-[-0.03em] text-balance text-foreground sm:text-6xl">
20
+ {title}
21
+ </h2>
22
+ {description ? (
23
+ <p className={cn("mt-5 max-w-xl text-pretty text-base leading-7 text-muted-foreground sm:text-lg", align === "center" && "mx-auto")}>
24
+ {description}
25
+ </p>
26
+ ) : null}
27
+ </div>
28
+ <CTAActions action={action} secondaryAction={secondaryAction} align={align} />
29
+ </div>
30
+ </Container>
31
+ </section>
32
+ ),
33
+ );
34
+ CTASurface.displayName = "CTASurface";
@@ -0,0 +1,48 @@
1
+ import type { FAQProps } from "./types";
2
+ import { FaqCards } from "./variants/Cards";
3
+ import { FaqAccordion } from "./variants/Accordion";
4
+ import { FaqSplit } from "./variants/Split";
5
+
6
+ export const FAQ = ({
7
+ className,
8
+ items,
9
+ option = "accordion",
10
+ allowMultiple = false,
11
+ defaultOpen = [],
12
+ eyebrow,
13
+ title,
14
+ description,
15
+ ...props
16
+ }: FAQProps) => {
17
+ if (option === "cards") {
18
+ return <FaqCards className={className} items={items} eyebrow={eyebrow} {...props} />;
19
+ }
20
+
21
+ if (option === "split") {
22
+ return (
23
+ <FaqSplit
24
+ className={className}
25
+ items={items}
26
+ allowMultiple={allowMultiple}
27
+ defaultOpen={defaultOpen}
28
+ eyebrow={eyebrow}
29
+ title={title}
30
+ description={description}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ // accordion
37
+ return (
38
+ <div className="mx-auto w-full max-w-3xl">
39
+ <FaqAccordion
40
+ className={className}
41
+ items={items}
42
+ allowMultiple={allowMultiple}
43
+ defaultOpen={defaultOpen}
44
+ />
45
+ </div>
46
+ );
47
+ };
48
+ FAQ.displayName = "FAQ";
@@ -0,0 +1,2 @@
1
+ export { FAQ } from "./FAQ";
2
+ export type { FAQProps, FAQOption, FaqItem } from "./types";
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { cn } from "../../utils/cn";
4
+ import { Plus } from "../../icons";
5
+ import type { FaqItem } from "./types";
6
+
7
+ export function useFaqState(allowMultiple: boolean, defaultOpen: number[]) {
8
+ const [openSet, setOpenSet] = useState<Set<number>>(
9
+ () => new Set(allowMultiple ? defaultOpen : defaultOpen.slice(0, 1)),
10
+ );
11
+
12
+ const toggle = (index: number) => {
13
+ setOpenSet((prev) => {
14
+ const next = new Set(prev);
15
+ if (next.has(index)) {
16
+ next.delete(index);
17
+ } else {
18
+ if (!allowMultiple) next.clear();
19
+ next.add(index);
20
+ }
21
+ return next;
22
+ });
23
+ };
24
+
25
+ return { openSet, toggle, isOpen: (i: number) => openSet.has(i) };
26
+ }
27
+
28
+ export function FaqItemRow({
29
+ item,
30
+ open,
31
+ onToggle,
32
+ style,
33
+ }: {
34
+ item: FaqItem;
35
+ open: boolean;
36
+ onToggle: () => void;
37
+ style: "boxed" | "bare";
38
+ }) {
39
+ return (
40
+ <div>
41
+ <h3>
42
+ <button
43
+ type="button"
44
+ aria-expanded={open}
45
+ onClick={onToggle}
46
+ className={
47
+ style === "boxed"
48
+ ? "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"
49
+ : "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"
50
+ }
51
+ >
52
+ {item.question}
53
+ <Plus
54
+ className={cn(
55
+ "h-4.5 w-4.5 shrink-0 text-foreground/45 transition-transform duration-200 sm:h-5 sm:w-5",
56
+ open && "rotate-45 text-primary",
57
+ )}
58
+ />
59
+ </button>
60
+ </h3>
61
+ {open ? (
62
+ <p className={style === "boxed" ? "px-6 pb-6 text-sm leading-7 text-muted-foreground" : "pb-6 text-sm leading-7 text-muted-foreground"}>
63
+ {item.answer}
64
+ </p>
65
+ ) : null}
66
+ </div>
67
+ );
68
+ }
69
+
70
+ export function FaqEyebrow({ label }: { label: string }) {
71
+ return (
72
+ <p className="text-xs font-semibold uppercase tracking-[0.18em] text-primary">{label}</p>
73
+ );
74
+ }