@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
@@ -1,180 +0,0 @@
1
- import { useState, type FormEvent, type HTMLAttributes } from "react";
2
- import { cn } from "../utils/cn";
3
- import { ArrowRight, Check, Mail } from "../icons";
4
-
5
- export interface NewsletterProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
6
- option?: "inline" | "card" | "underline";
7
- placeholder?: string;
8
- buttonLabel?: string;
9
- successMessage?: string;
10
- note?: string;
11
- onSubmit?: (email: string) => void;
12
- }
13
-
14
- const inputBase =
15
- "w-full rounded-lg border border-border bg-surface px-4 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background";
16
-
17
- export const Newsletter = ({
18
- className,
19
- option = "inline",
20
- placeholder = "you@company.com",
21
- buttonLabel = "Join",
22
- successMessage = "You’re on the list!",
23
- note,
24
- onSubmit,
25
- ...props
26
- }: NewsletterProps) => {
27
- const [email, setEmail] = useState("");
28
- const [done, setDone] = useState(false);
29
- const [error, setError] = useState<string | null>(null);
30
-
31
- const handleSubmit = (event: FormEvent) => {
32
- event.preventDefault();
33
- const value = email.trim();
34
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
35
- setError("Please enter a valid email.");
36
- return;
37
- }
38
- setError(null);
39
- setDone(true);
40
- onSubmit?.(value);
41
- };
42
-
43
- const success = (
44
- <p className="flex items-center gap-2 rounded-lg border border-accent/40 bg-accent/10 px-4 py-3.5 text-sm font-medium text-accent" role="status">
45
- <Check className="h-4 w-4" />
46
- {successMessage}
47
- </p>
48
- );
49
-
50
- if (option === "card") {
51
- return (
52
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
53
- {done ? (
54
- success
55
- ) : (
56
- <div className="rounded-xl border border-border bg-surface p-7">
57
- <div className="mb-5 flex items-center gap-3">
58
- <span className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-primary-soft text-primary">
59
- <Mail className="h-4 w-4" />
60
- </span>
61
- <p className="font-display text-lg font-semibold tracking-tight text-foreground">
62
- Monthly digest
63
- </p>
64
- </div>
65
- <label htmlFor="newsletter-email" className="sr-only">
66
- Your email address
67
- </label>
68
- <div className="flex flex-col gap-3 sm:flex-row">
69
- <input
70
- id="newsletter-email"
71
- type="email"
72
- autoComplete="email"
73
- value={email}
74
- onChange={(e) => {
75
- setEmail(e.target.value);
76
- if (error) setError(null);
77
- }}
78
- placeholder={placeholder}
79
- className={cn(inputBase, "h-11")}
80
- />
81
- <button
82
- type="submit"
83
- className="inline-flex h-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-5 text-sm font-semibold text-on-primary transition-colors duration-200 hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
84
- >
85
- {buttonLabel}
86
- <ArrowRight className="h-4 w-4 opacity-60" />
87
- </button>
88
- </div>
89
- {error ? (
90
- <p className="mt-2 text-sm font-medium text-red-500" role="alert">{error}</p>
91
- ) : note ? (
92
- <p className="mt-3 text-xs leading-5 text-muted-foreground">{note}</p>
93
- ) : null}
94
- </div>
95
- )}
96
- </form>
97
- );
98
- }
99
-
100
- if (option === "underline") {
101
- return (
102
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
103
- {done ? (
104
- success
105
- ) : (
106
- <div className="flex items-end gap-3 border-b border-foreground/20 pb-3 transition-colors duration-200 focus-within:border-foreground">
107
- <label htmlFor="newsletter-email" className="sr-only">
108
- Your email address
109
- </label>
110
- <input
111
- id="newsletter-email"
112
- type="email"
113
- autoComplete="email"
114
- value={email}
115
- onChange={(e) => {
116
- setEmail(e.target.value);
117
- if (error) setError(null);
118
- }}
119
- placeholder={placeholder}
120
- className="w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
121
- />
122
- <button
123
- type="submit"
124
- className="inline-flex shrink-0 cursor-pointer items-center gap-1.5 text-sm font-semibold text-foreground transition-colors duration-150 hover:text-primary"
125
- >
126
- {buttonLabel}
127
- <ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
128
- </button>
129
- </div>
130
- )}
131
- {error ? (
132
- <p className="mt-2 text-sm font-medium text-red-500" role="alert">{error}</p>
133
- ) : note ? (
134
- <p className="mt-2 text-xs leading-5 text-muted-foreground">{note}</p>
135
- ) : null}
136
- </form>
137
- );
138
- }
139
-
140
- return (
141
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
142
- {done ? (
143
- success
144
- ) : (
145
- <>
146
- <label htmlFor="newsletter-email" className="sr-only">
147
- Your email address
148
- </label>
149
- <div className="flex flex-col gap-3 sm:flex-row">
150
- <input
151
- id="newsletter-email"
152
- type="email"
153
- autoComplete="email"
154
- value={email}
155
- onChange={(e) => {
156
- setEmail(e.target.value);
157
- if (error) setError(null);
158
- }}
159
- placeholder={placeholder}
160
- className={cn(inputBase, "h-11")}
161
- />
162
- <button
163
- type="submit"
164
- className="inline-flex h-11 shrink-0 cursor-pointer items-center justify-center gap-2 rounded-lg bg-primary px-5 text-sm font-semibold text-on-primary transition-colors duration-200 hover:bg-primary-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
165
- >
166
- {buttonLabel}
167
- <ArrowRight className="h-4 w-4 opacity-60" />
168
- </button>
169
- </div>
170
- {error ? (
171
- <p className="mt-2 text-sm font-medium text-red-500" role="alert">{error}</p>
172
- ) : note ? (
173
- <p className="mt-2 text-xs leading-5 text-muted-foreground">{note}</p>
174
- ) : null}
175
- </>
176
- )}
177
- </form>
178
- );
179
- };
180
- Newsletter.displayName = "Newsletter";
@@ -1,336 +0,0 @@
1
- import { useState, type HTMLAttributes } from "react";
2
- import { cn } from "../utils/cn";
3
- import { getContrastText } from "../utils/contrast";
4
- import { useTokenForeground } from "../utils/useTokenForeground";
5
- import { ArrowRight, Check } from "../icons";
6
-
7
- export interface Plan {
8
- name: string;
9
- description?: string;
10
- monthly: number | null;
11
- yearly: number | null;
12
- features: string[];
13
- cta?: string;
14
- href?: string;
15
- highlighted?: boolean;
16
- customColor?: string;
17
- }
18
-
19
- export interface PricingProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
20
- plans: Plan[];
21
- option?: "cards" | "bento" | "compact";
22
- defaultBilling?: "monthly" | "yearly";
23
- onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
24
- }
25
-
26
- function PriceValue({
27
- value,
28
- billing,
29
- dim,
30
- }: {
31
- value: number | null;
32
- billing: "monthly" | "yearly";
33
- /** False when rendered on a colored background where opacity would kill contrast. */
34
- dim?: boolean;
35
- }) {
36
- if (value === null) {
37
- return <span>Custom</span>;
38
- }
39
- return (
40
- <>
41
- <span className="text-[2.6rem] font-semibold tracking-tight sm:text-[3rem]">${value}</span>
42
- <span className={cn("text-sm font-normal", dim && "opacity-60")}>
43
- /{billing === "monthly" ? "mo" : "yr"}
44
- </span>
45
- </>
46
- );
47
- }
48
-
49
- function BillingToggle({
50
- billing,
51
- onChange,
52
- dark,
53
- }: {
54
- billing: "monthly" | "yearly";
55
- onChange: (b: "monthly" | "yearly") => void;
56
- dark?: boolean;
57
- }) {
58
- // The accent token is a background color, but its shipped "on-accent"
59
- // pairing isn't guaranteed to be contrast-safe (e.g. white on amber).
60
- // Derive the badge text color from the actual accent value instead.
61
- const accentText = useTokenForeground("--color-accent");
62
- return (
63
- <div className="mb-10 flex items-center justify-center gap-4">
64
- <span
65
- className={cn(
66
- "text-sm font-medium transition-colors",
67
- billing === "monthly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
68
- )}
69
- >
70
- Monthly
71
- </span>
72
- <button
73
- type="button"
74
- role="switch"
75
- aria-checked={billing === "yearly"}
76
- aria-label="Billing period"
77
- onClick={() => onChange(billing === "monthly" ? "yearly" : "monthly")}
78
- className={cn(
79
- "relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200",
80
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
81
- billing === "yearly" ? "bg-primary" : "bg-border",
82
- )}
83
- >
84
- <span
85
- className={cn(
86
- "inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200",
87
- billing === "yearly" ? "translate-x-6" : "translate-x-1",
88
- )}
89
- />
90
- </button>
91
- <span
92
- className={cn(
93
- "flex items-center gap-2 text-sm font-medium transition-colors",
94
- billing === "yearly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
95
- )}
96
- >
97
- Yearly
98
- <span
99
- className="rounded-full bg-accent px-2 py-0.5 text-xs font-semibold"
100
- style={{ color: accentText }}
101
- >
102
- −2 ay
103
- </span>
104
- </span>
105
- </div>
106
- );
107
- }
108
-
109
- export const Pricing = ({
110
- className,
111
- plans,
112
- option = "cards",
113
- defaultBilling = "monthly",
114
- onSelect,
115
- ...props
116
- }: PricingProps) => {
117
- const [billing, setBilling] = useState<"monthly" | "yearly">(defaultBilling);
118
-
119
- const price = (plan: Plan) => (billing === "monthly" ? plan.monthly : plan.yearly);
120
- const priceLabel = (v: number | null) => (v === null ? "Custom" : `$${v}`);
121
-
122
- const planButton = (plan: Plan, dark?: boolean) => {
123
- const isHighlighted = plan.highlighted && !plan.customColor;
124
- const fg = plan.customColor ? getContrastText(plan.customColor, "#111111", "#ffffff") : undefined;
125
- return (
126
- <button
127
- type="button"
128
- onClick={() => onSelect?.(plan, billing)}
129
- className={cn(
130
- "inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-lg px-5 text-sm font-semibold transition-colors duration-200",
131
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
132
- isHighlighted || plan.customColor
133
- ? "hover:brightness-95"
134
- : dark
135
- ? "bg-white text-black hover:bg-white/90"
136
- : "border border-border text-foreground hover:bg-surface",
137
- )}
138
- style={
139
- plan.customColor
140
- ? { backgroundColor: plan.customColor, color: fg }
141
- : isHighlighted
142
- ? {
143
- backgroundColor: "rgb(var(--color-primary))",
144
- color: "rgb(var(--color-on-primary))",
145
- }
146
- : undefined
147
- }
148
- >
149
- {plan.cta || "Start"}
150
- <ArrowRight className="h-4 w-4 opacity-60" />
151
- </button>
152
- );
153
- };
154
-
155
- if (option === "compact") {
156
- return (
157
- <div className={cn("w-full", className)} {...props}>
158
- <div className="divide-y divide-border overflow-hidden rounded-xl border border-border bg-surface">
159
- {plans.map((plan) => {
160
- const isHighlighted = plan.highlighted && !plan.customColor;
161
- const bg = plan.customColor ? "rgb(var(--color-surface))" : isHighlighted ? "rgb(var(--color-primary-soft))" : undefined;
162
- const fg = plan.customColor ? plan.customColor : isHighlighted ? "rgb(var(--color-primary))" : undefined;
163
- return (
164
- <div
165
- key={plan.name}
166
- className={cn(
167
- "group flex flex-col gap-5 p-5 transition-colors duration-200 hover:bg-surface-strong sm:flex-row sm:items-center sm:justify-between",
168
- isHighlighted && "bg-primary-soft/60",
169
- )}
170
- style={bg ? { backgroundColor: bg } : undefined}
171
- >
172
- <div className="min-w-0">
173
- <div className="flex items-center gap-2.5">
174
- <h3 className="font-display text-lg font-semibold tracking-tight text-foreground">
175
- {plan.name}
176
- </h3>
177
- {isHighlighted ? (
178
- <span className="rounded-full bg-primary px-2.5 py-0.5 text-[11px] font-semibold text-on-primary uppercase tracking-wider">
179
- Popular
180
- </span>
181
- ) : null}
182
- </div>
183
- {plan.description ? (
184
- <p className="mt-1 truncate text-sm text-muted-foreground">{plan.description}</p>
185
- ) : null}
186
- </div>
187
- <div className="flex items-center gap-6">
188
- <ul className="hidden items-center gap-4 lg:flex">
189
- {plan.features.slice(0, 3).map((f) => (
190
- <li key={f} className="flex items-center gap-1.5 text-sm text-muted-foreground" style={{ color: fg }}>
191
- <Check className="h-3.5 w-3.5 text-current" />
192
- {f}
193
- </li>
194
- ))}
195
- </ul>
196
- <div className="font-display text-2xl font-semibold tracking-tight text-foreground">
197
- {priceLabel(price(plan))}
198
- {price(plan) !== null ? (
199
- <span className="text-sm font-normal text-muted-foreground">/{billing === "monthly" ? "mo" : "yr"}</span>
200
- ) : null}
201
- </div>
202
- {planButton(plan)}
203
- </div>
204
- </div>
205
- );
206
- })}
207
- </div>
208
- </div>
209
- );
210
- }
211
-
212
- if (option === "bento") {
213
- return (
214
- <div className={cn("w-full", className)} {...props}>
215
- <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
216
- {plans.map((plan) => {
217
- const isHighlighted = plan.highlighted && !plan.customColor;
218
- const bg = plan.customColor || (isHighlighted ? "rgb(var(--color-primary))" : undefined);
219
- const fg = plan.customColor
220
- ? getContrastText(plan.customColor, "#111111", "#ffffff")
221
- : isHighlighted
222
- ? "rgb(var(--color-on-primary))"
223
- : undefined;
224
- return (
225
- <div
226
- key={plan.name}
227
- className={cn(
228
- "flex h-full flex-col rounded-xl p-8",
229
- bg
230
- ? "lg:col-span-2"
231
- : "border border-border bg-surface",
232
- )}
233
- style={bg ? { backgroundColor: bg, color: fg } : undefined}
234
- >
235
- <div className="flex flex-wrap items-start justify-between gap-4">
236
- <div>
237
- <h3 className={cn("font-display text-xl font-semibold tracking-tight", bg ? "text-current" : "text-foreground")}>
238
- {plan.name}
239
- </h3>
240
- {plan.description ? (
241
- <p className={cn("mt-1.5 text-sm", bg ? undefined : "text-muted-foreground")}>
242
- {plan.description}
243
- </p>
244
- ) : null}
245
- </div>
246
- <div className={cn("font-display", bg ? "text-current" : "text-foreground")}>
247
- <PriceValue value={price(plan)} billing={billing} dim={!bg} />
248
- </div>
249
- </div>
250
- <ul
251
- className={cn(
252
- "mt-7 grid grid-cols-1 gap-x-8 gap-y-2.5 text-sm sm:grid-cols-2",
253
- bg ? "lg:max-w-2xl" : undefined,
254
- )}
255
- >
256
- {plan.features.map((feature) => (
257
- <li key={feature} className={cn("flex items-start gap-2.5", bg ? "text-current" : "text-muted-foreground")}>
258
- <Check className={cn("mt-0.5 h-4 w-4 shrink-0", bg ? "text-current" : "text-primary")} />
259
- {feature}
260
- </li>
261
- ))}
262
- </ul>
263
- <div className="mt-8">{planButton(plan, !bg)}</div>
264
- </div>
265
- );
266
- })}
267
- </div>
268
- </div>
269
- );
270
- }
271
-
272
- return (
273
- <div className={cn("w-full", className)} {...props}>
274
- <BillingToggle billing={billing} onChange={setBilling} />
275
- <div className="grid grid-cols-1 gap-5 md:grid-cols-2 lg:grid-cols-3">
276
- {plans.map((plan) => {
277
- const isHighlighted = plan.highlighted && !plan.customColor;
278
- const bg = plan.customColor || (isHighlighted ? "rgb(var(--color-primary))" : undefined);
279
- const fg = plan.customColor
280
- ? getContrastText(plan.customColor, "#111111", "#ffffff")
281
- : isHighlighted
282
- ? "rgb(var(--color-on-primary))"
283
- : undefined;
284
- return (
285
- <div
286
- key={plan.name}
287
- className={cn(
288
- "relative flex flex-col rounded-xl p-7",
289
- bg ? "shadow-raised" : "border border-border bg-surface",
290
- !bg && isHighlighted && "border-primary",
291
- )}
292
- style={bg ? { backgroundColor: bg, color: fg } : undefined}
293
- >
294
- {isHighlighted ? (
295
- <span
296
- className={cn(
297
- "absolute -top-3 left-7 rounded-full px-2.5 py-1 text-[11px] font-semibold uppercase tracking-wider",
298
- !bg && "bg-primary text-on-primary",
299
- )}
300
- style={
301
- bg
302
- ? { backgroundColor: "rgb(var(--color-on-primary))", color: "rgb(var(--color-primary))" }
303
- : undefined
304
- }
305
- >
306
- Most popular
307
- </span>
308
- ) : null}
309
- <h3 className={cn("font-display text-lg font-semibold tracking-tight", bg ? "text-current" : "text-foreground")}>
310
- {plan.name}
311
- </h3>
312
- {plan.description ? (
313
- <p className={cn("mt-1.5 text-sm", bg ? undefined : "text-muted-foreground")}>
314
- {plan.description}
315
- </p>
316
- ) : null}
317
- <div className={cn("mt-6 flex items-baseline font-display", bg ? "text-current" : "text-foreground")}>
318
- <PriceValue value={price(plan)} billing={billing} dim={!bg} />
319
- </div>
320
- <ul className={cn("mt-7 flex-1 space-y-2.5 text-sm", bg ? "text-current" : "text-muted-foreground")}>
321
- {plan.features.map((feature) => (
322
- <li key={feature} className="flex items-start gap-2.5">
323
- <Check className={cn("mt-0.5 h-4 w-4 shrink-0", bg ? "text-current" : "text-primary")} />
324
- {feature}
325
- </li>
326
- ))}
327
- </ul>
328
- <div className="mt-8">{planButton(plan, !bg)}</div>
329
- </div>
330
- );
331
- })}
332
- </div>
333
- </div>
334
- );
335
- };
336
- Pricing.displayName = "Pricing";