@ugurdemirel/landcraft 0.1.4 → 0.1.9

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 (112) hide show
  1. package/dist/styles.css +12 -0
  2. package/package.json +14 -7
  3. package/src/components/Badge.tsx +0 -68
  4. package/src/components/Blog/BlogCard.tsx +0 -12
  5. package/src/components/Blog/BlogSection.tsx +0 -95
  6. package/src/components/Blog/index.ts +0 -8
  7. package/src/components/Blog/parts.tsx +0 -38
  8. package/src/components/Blog/types.ts +0 -42
  9. package/src/components/Blog/variants/Card.tsx +0 -62
  10. package/src/components/Blog/variants/Row.tsx +0 -41
  11. package/src/components/Button.tsx +0 -113
  12. package/src/components/CTA/CTA.tsx +0 -12
  13. package/src/components/CTA/index.ts +0 -2
  14. package/src/components/CTA/parts.tsx +0 -26
  15. package/src/components/CTA/types.ts +0 -12
  16. package/src/components/CTA/variants/Inverse.tsx +0 -50
  17. package/src/components/CTA/variants/Panel.tsx +0 -60
  18. package/src/components/CTA/variants/Surface.tsx +0 -34
  19. package/src/components/Card.tsx +0 -80
  20. package/src/components/Container.tsx +0 -86
  21. package/src/components/FAQ/FAQ.tsx +0 -48
  22. package/src/components/FAQ/index.ts +0 -2
  23. package/src/components/FAQ/parts.tsx +0 -74
  24. package/src/components/FAQ/types.ts +0 -18
  25. package/src/components/FAQ/variants/Accordion.tsx +0 -46
  26. package/src/components/FAQ/variants/Cards.tsx +0 -40
  27. package/src/components/FAQ/variants/Split.tsx +0 -44
  28. package/src/components/Features/FeatureCard.tsx +0 -44
  29. package/src/components/Features/FeatureGrid.tsx +0 -14
  30. package/src/components/Features/index.ts +0 -3
  31. package/src/components/Features/parts.ts +0 -15
  32. package/src/components/Features/types.ts +0 -25
  33. package/src/components/Features/variants/Bento.tsx +0 -32
  34. package/src/components/Features/variants/Columns.tsx +0 -21
  35. package/src/components/Features/variants/EditorialRows.tsx +0 -38
  36. package/src/components/Footer/Footer.tsx +0 -11
  37. package/src/components/Footer/index.ts +0 -2
  38. package/src/components/Footer/parts.tsx +0 -101
  39. package/src/components/Footer/types.ts +0 -32
  40. package/src/components/Footer/variants/Classic.tsx +0 -59
  41. package/src/components/Footer/variants/Editorial.tsx +0 -47
  42. package/src/components/Footer/variants/Minimal.tsx +0 -33
  43. package/src/components/Hero/Hero.tsx +0 -12
  44. package/src/components/Hero/index.ts +0 -2
  45. package/src/components/Hero/parts.tsx +0 -29
  46. package/src/components/Hero/types.ts +0 -20
  47. package/src/components/Hero/variants/Centered.tsx +0 -54
  48. package/src/components/Hero/variants/Split.tsx +0 -53
  49. package/src/components/Hero/variants/Statement.tsx +0 -60
  50. package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +0 -37
  51. package/src/components/LanguageSwitcher/index.ts +0 -6
  52. package/src/components/LanguageSwitcher/parts.tsx +0 -144
  53. package/src/components/LanguageSwitcher/types.ts +0 -40
  54. package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +0 -23
  55. package/src/components/LanguageSwitcher/variants/Dropdown.tsx +0 -88
  56. package/src/components/LanguageSwitcher/variants/Modal.tsx +0 -56
  57. package/src/components/LogoCloud/LogoCloud.tsx +0 -14
  58. package/src/components/LogoCloud/index.ts +0 -2
  59. package/src/components/LogoCloud/parts.tsx +0 -32
  60. package/src/components/LogoCloud/types.ts +0 -16
  61. package/src/components/LogoCloud/variants/Marquee.tsx +0 -30
  62. package/src/components/LogoCloud/variants/Quiet.tsx +0 -26
  63. package/src/components/LogoCloud/variants/Strip.tsx +0 -25
  64. package/src/components/MegaMenu/MegaMenu.tsx +0 -454
  65. package/src/components/MegaMenu/index.ts +0 -9
  66. package/src/components/MegaMenu/types.ts +0 -56
  67. package/src/components/Modal.tsx +0 -125
  68. package/src/components/Navbar/Navbar.tsx +0 -185
  69. package/src/components/Navbar/index.ts +0 -2
  70. package/src/components/Navbar/types.ts +0 -30
  71. package/src/components/Newsletter/Newsletter.tsx +0 -12
  72. package/src/components/Newsletter/index.ts +0 -2
  73. package/src/components/Newsletter/parts.tsx +0 -23
  74. package/src/components/Newsletter/types.ts +0 -12
  75. package/src/components/Newsletter/useNewsletterForm.ts +0 -27
  76. package/src/components/Newsletter/variants/Card.tsx +0 -62
  77. package/src/components/Newsletter/variants/Inline.tsx +0 -54
  78. package/src/components/Newsletter/variants/Underline.tsx +0 -52
  79. package/src/components/Pricing/Pricing.tsx +0 -37
  80. package/src/components/Pricing/index.ts +0 -2
  81. package/src/components/Pricing/parts.tsx +0 -132
  82. package/src/components/Pricing/types.ts +0 -28
  83. package/src/components/Pricing/variants/Bento.tsx +0 -72
  84. package/src/components/Pricing/variants/Cards.tsx +0 -79
  85. package/src/components/Pricing/variants/Compact.tsx +0 -69
  86. package/src/components/Prose.tsx +0 -55
  87. package/src/components/Section.tsx +0 -106
  88. package/src/components/Slot.tsx +0 -78
  89. package/src/components/Stats/Stats.tsx +0 -16
  90. package/src/components/Stats/index.ts +0 -2
  91. package/src/components/Stats/parts.tsx +0 -29
  92. package/src/components/Stats/types.ts +0 -23
  93. package/src/components/Stats/variants/Cells.tsx +0 -34
  94. package/src/components/Stats/variants/Editorial.tsx +0 -41
  95. package/src/components/Stats/variants/Hairline.tsx +0 -31
  96. package/src/components/Stats/variants/Ticker.tsx +0 -36
  97. package/src/components/Testimonials/TestimonialCard.tsx +0 -61
  98. package/src/components/Testimonials/Testimonials.tsx +0 -14
  99. package/src/components/Testimonials/index.ts +0 -3
  100. package/src/components/Testimonials/parts.ts +0 -7
  101. package/src/components/Testimonials/types.ts +0 -24
  102. package/src/components/Testimonials/variants/Carousel.tsx +0 -94
  103. package/src/components/Testimonials/variants/Grid.tsx +0 -19
  104. package/src/components/Testimonials/variants/Marquee.tsx +0 -28
  105. package/src/icons.tsx +0 -290
  106. package/src/index.ts +0 -120
  107. package/src/styles/index.css +0 -18
  108. package/src/utils/cn.ts +0 -6
  109. package/src/utils/contrast.ts +0 -107
  110. package/src/utils/iso2Flag.ts +0 -26
  111. package/src/utils/useTokenForeground.ts +0 -39
  112. /package/{src/styles → dist}/theme.css +0 -0
@@ -1,185 +0,0 @@
1
- "use client";
2
- import { useState } from "react";
3
- import { cn } from "../../utils/cn";
4
- import { Menu, X } from "../../icons";
5
- import type { NavbarProps, NavbarVariant } from "./types";
6
-
7
- interface NavbarStyle {
8
- shell: string;
9
- bar: string;
10
- header: string;
11
- link: string;
12
- mobilePanel: string;
13
- mobileLink: string;
14
- menuButton: string;
15
- brandText: string;
16
- brandDot: string;
17
- desktopList: string;
18
- }
19
-
20
- const inverse = "text-on-secondary";
21
- const light = "text-foreground";
22
-
23
- const styles: Record<NavbarVariant, NavbarStyle> = {
24
- classic: {
25
- shell: "border-b border-border bg-background/85 backdrop-blur-md supports-[backdrop-filter]:bg-background/75",
26
- bar: "mx-auto max-w-6xl px-5 sm:px-8",
27
- header: "px-0",
28
- link: "text-muted-foreground hover:text-foreground",
29
- mobilePanel: "border-t border-border bg-background",
30
- mobileLink:
31
- "text-muted-foreground hover:bg-surface hover:text-foreground",
32
- menuButton: `${light} hover:bg-surface`,
33
- brandText: light,
34
- brandDot: "bg-foreground",
35
- desktopList: "hidden items-center gap-7 md:flex",
36
- },
37
- floating: {
38
- shell: "px-0",
39
- bar: "mx-auto mt-3 max-w-5xl rounded-xl border border-border bg-surface/90 px-5 shadow-soft backdrop-blur-md supports-[backdrop-filter]:bg-surface/70",
40
- header: "px-3 sm:px-6",
41
- link: "text-muted-foreground hover:text-foreground",
42
- mobilePanel:
43
- "mx-3 mb-3 max-w-5xl overflow-hidden rounded-b-xl border border-border bg-surface",
44
- mobileLink:
45
- "text-muted-foreground hover:bg-surface hover:text-foreground",
46
- menuButton: `${light} hover:bg-surface`,
47
- brandText: light,
48
- brandDot: "bg-foreground",
49
- desktopList: "hidden items-center gap-7 md:flex",
50
- },
51
- inverse: {
52
- shell: "border-b border-white/10 bg-[#101010]/85 backdrop-blur-md",
53
- bar: "mx-auto max-w-6xl px-5 sm:px-8",
54
- header: "px-0",
55
- link: "text-on-secondary/60 hover:text-on-secondary",
56
- mobilePanel: "border-t border-white/10 bg-[#101010]",
57
- mobileLink:
58
- "text-on-secondary/70 hover:bg-white/10 hover:text-on-secondary",
59
- menuButton: `${inverse} hover:bg-white/10`,
60
- brandText: inverse,
61
- brandDot: "bg-on-secondary",
62
- desktopList: "hidden items-center gap-7 md:flex",
63
- },
64
- };
65
-
66
- export const Navbar = ({
67
- className,
68
- variant = "classic",
69
- brand,
70
- logo,
71
- logoSrc,
72
- logoAlt,
73
- logoClassName,
74
- brandHref = "#",
75
- links,
76
- actions,
77
- cta,
78
- languageSwitcher,
79
- sticky = true,
80
- LinkComponent,
81
- ...props
82
- }: NavbarProps) => {
83
- const [open, setOpen] = useState(false);
84
- const Link = LinkComponent ?? "a";
85
- const s = styles[variant];
86
-
87
- const logoNode =
88
- logo ??
89
- (logoSrc ? (
90
- <img
91
- src={logoSrc}
92
- alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
93
- className={cn("h-7 w-auto", logoClassName)}
94
- />
95
- ) : null);
96
-
97
- const brandNode = logoNode ? (
98
- <span className="flex items-center">{logoNode}</span>
99
- ) : (
100
- <span className={cn("flex items-center gap-2 font-display text-[17px] font-bold tracking-tight", s.brandText)}>
101
- <span aria-hidden className={cn("inline-block h-2.5 w-2.5 rounded-sm", s.brandDot)} />
102
- {brand}
103
- </span>
104
- );
105
-
106
- return (
107
- <header className={cn("w-full transition-colors duration-200", sticky && "sticky top-0 z-50", s.shell, s.header, className)} {...props}>
108
- <nav className={cn("flex h-16 w-full items-center justify-between gap-4 transition-colors duration-200", s.bar)} aria-label="Main navigation">
109
- <Link href={brandHref} className="shrink-0">
110
- {brandNode}
111
- </Link>
112
-
113
- <ul className={s.desktopList}>
114
- {links.map((link) => (
115
- <li key={link.href}>
116
- <Link
117
- href={link.href}
118
- className={cn("text-sm font-medium transition-colors duration-150", s.link)}
119
- >
120
- {link.label}
121
- </Link>
122
- </li>
123
- ))}
124
- </ul>
125
-
126
- <div className="hidden items-center gap-3 md:flex">
127
- {languageSwitcher}
128
- {actions}
129
- {cta}
130
- </div>
131
-
132
- {languageSwitcher ? (
133
- <div className="flex items-center md:hidden">{languageSwitcher}</div>
134
- ) : null}
135
-
136
- <button
137
- type="button"
138
- aria-label={open ? "Close menu" : "Open menu"}
139
- aria-expanded={open}
140
- onClick={() => setOpen((o) => !o)}
141
- className={cn(
142
- "inline-flex h-10 w-10 shrink-0 cursor-pointer items-center justify-center rounded-md md:hidden",
143
- "transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
144
- s.menuButton,
145
- )}
146
- >
147
- {open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
148
- </button>
149
- </nav>
150
-
151
- <div
152
- className={cn(
153
- "grid transition-[grid-template-rows,opacity] duration-200 md:hidden",
154
- open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
155
- s.mobilePanel,
156
- )}
157
- >
158
- <div className="min-h-0 overflow-hidden">
159
- <ul className={cn("flex flex-col py-4", variant !== "floating" && "px-5")}>
160
- {links.map((link) => (
161
- <li key={link.href}>
162
- <Link
163
- href={link.href}
164
- onClick={() => setOpen(false)}
165
- className={cn(
166
- "block rounded-md px-2 py-2.5 text-sm font-medium transition-colors duration-150",
167
- s.mobileLink,
168
- )}
169
- >
170
- {link.label}
171
- </Link>
172
- </li>
173
- ))}
174
- {cta ? (
175
- <li className="mt-3 px-2 pb-2">{cta}</li>
176
- ) : actions ? (
177
- <li className="mt-3 flex flex-wrap gap-3 px-2 pb-2">{actions}</li>
178
- ) : null}
179
- </ul>
180
- </div>
181
- </div>
182
- </header>
183
- );
184
- };
185
- Navbar.displayName = "Navbar";
@@ -1,2 +0,0 @@
1
- export { Navbar } from "./Navbar";
2
- export type { NavbarProps, NavbarVariant, NavLink } from "./types";
@@ -1,30 +0,0 @@
1
- import type { ElementType, HTMLAttributes, ReactNode } from "react";
2
-
3
- export interface NavLink {
4
- label: string;
5
- href: string;
6
- }
7
-
8
- export type NavbarVariant = "classic" | "floating" | "inverse";
9
-
10
- export interface NavbarProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
11
- variant?: NavbarVariant;
12
- /** Wordmark or fallback text rendered when no logo is supplied. */
13
- brand?: ReactNode;
14
- /** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
15
- logo?: ReactNode;
16
- /** Quick logo image source. */
17
- logoSrc?: string;
18
- logoAlt?: string;
19
- /** Size / position tuning for logo images. Defaults to `h-7 w-auto`. */
20
- logoClassName?: string;
21
- brandHref?: string;
22
- links: NavLink[];
23
- actions?: ReactNode;
24
- cta?: ReactNode;
25
- /** Optional language switcher rendered on the right (desktop) and beside the menu button (mobile). */
26
- languageSwitcher?: ReactNode;
27
- sticky?: boolean;
28
- /** Component used to render the brand and nav links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
29
- LinkComponent?: ElementType;
30
- }
@@ -1,12 +0,0 @@
1
- import type { NewsletterProps } from "./types";
2
- import { NewsletterInline } from "./variants/Inline";
3
- import { NewsletterCard } from "./variants/Card";
4
- import { NewsletterUnderline } from "./variants/Underline";
5
-
6
- export const Newsletter = (props: NewsletterProps) => {
7
- const { option = "inline", ...rest } = props;
8
- if (option === "card") return <NewsletterCard {...rest} />;
9
- if (option === "underline") return <NewsletterUnderline {...rest} />;
10
- return <NewsletterInline {...rest} />;
11
- };
12
- Newsletter.displayName = "Newsletter";
@@ -1,2 +0,0 @@
1
- export { Newsletter } from "./Newsletter";
2
- export type { NewsletterProps, NewsletterOption } from "./types";
@@ -1,23 +0,0 @@
1
- import { Check } from "../../icons";
2
-
3
- export const inputBase =
4
- "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";
5
-
6
- export function NewsletterSuccess({ message }: { message: string }) {
7
- return (
8
- <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">
9
- <Check className="h-4 w-4" />
10
- {message}
11
- </p>
12
- );
13
- }
14
-
15
- export function NewsletterNote({ error, note }: { error: string | null; note?: string }) {
16
- if (error) {
17
- return <p className="mt-2 text-sm font-medium text-red-500" role="alert">{error}</p>;
18
- }
19
- if (note) {
20
- return <p className="mt-3 text-xs leading-5 text-muted-foreground">{note}</p>;
21
- }
22
- return null;
23
- }
@@ -1,12 +0,0 @@
1
- import type { HTMLAttributes } from "react";
2
-
3
- export type NewsletterOption = "inline" | "card" | "underline";
4
-
5
- export interface NewsletterProps extends Omit<HTMLAttributes<HTMLFormElement>, "onSubmit"> {
6
- option?: NewsletterOption;
7
- placeholder?: string;
8
- buttonLabel?: string;
9
- successMessage?: string;
10
- note?: string;
11
- onSubmit?: (email: string) => void;
12
- }
@@ -1,27 +0,0 @@
1
- "use client";
2
- import { useState, type FormEvent } from "react";
3
-
4
- export function useNewsletterForm(onSubmit?: (email: string) => void) {
5
- const [email, setEmail] = useState("");
6
- const [done, setDone] = useState(false);
7
- const [error, setError] = useState<string | null>(null);
8
-
9
- const handleSubmit = (event: FormEvent) => {
10
- event.preventDefault();
11
- const value = email.trim();
12
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
13
- setError("Please enter a valid email.");
14
- return;
15
- }
16
- setError(null);
17
- setDone(true);
18
- onSubmit?.(value);
19
- };
20
-
21
- const updateEmail = (value: string) => {
22
- setEmail(value);
23
- if (error) setError(null);
24
- };
25
-
26
- return { email, done, error, handleSubmit, updateEmail };
27
- }
@@ -1,62 +0,0 @@
1
- "use client";
2
- import { cn } from "../../../utils/cn";
3
- import { ArrowRight, Mail } from "../../../icons";
4
- import { useNewsletterForm } from "../useNewsletterForm";
5
- import { inputBase, NewsletterSuccess, NewsletterNote } from "../parts";
6
- import type { NewsletterProps } from "../types";
7
-
8
- interface CardProps extends Omit<NewsletterProps, "option"> {}
9
-
10
- export function NewsletterCard({
11
- className,
12
- placeholder = "you@company.com",
13
- buttonLabel = "Join",
14
- successMessage = "You’re on the list!",
15
- note,
16
- onSubmit,
17
- ...props
18
- }: CardProps) {
19
- const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
20
-
21
- return (
22
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
23
- {done ? (
24
- <NewsletterSuccess message={successMessage} />
25
- ) : (
26
- <div className="rounded-xl border border-border bg-surface p-7">
27
- <div className="mb-5 flex items-center gap-3">
28
- <span className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-primary-soft text-primary">
29
- <Mail className="h-4 w-4" />
30
- </span>
31
- <p className="font-display text-lg font-semibold tracking-tight text-foreground">
32
- Monthly digest
33
- </p>
34
- </div>
35
- <label htmlFor="newsletter-email" className="sr-only">
36
- Your email address
37
- </label>
38
- <div className="flex flex-col gap-3 sm:flex-row">
39
- <input
40
- id="newsletter-email"
41
- type="email"
42
- autoComplete="email"
43
- value={email}
44
- onChange={(e) => updateEmail(e.target.value)}
45
- placeholder={placeholder}
46
- className={cn(inputBase, "h-11")}
47
- />
48
- <button
49
- type="submit"
50
- 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"
51
- >
52
- {buttonLabel}
53
- <ArrowRight className="h-4 w-4 opacity-60" />
54
- </button>
55
- </div>
56
- <NewsletterNote error={error} note={note} />
57
- </div>
58
- )}
59
- </form>
60
- );
61
- }
62
- NewsletterCard.displayName = "NewsletterCard";
@@ -1,54 +0,0 @@
1
- "use client";
2
- import { cn } from "../../../utils/cn";
3
- import { ArrowRight } from "../../../icons";
4
- import { useNewsletterForm } from "../useNewsletterForm";
5
- import { inputBase, NewsletterSuccess, NewsletterNote } from "../parts";
6
- import type { NewsletterProps } from "../types";
7
-
8
- interface InlineProps extends Omit<NewsletterProps, "option"> {}
9
-
10
- export function NewsletterInline({
11
- className,
12
- placeholder = "you@company.com",
13
- buttonLabel = "Join",
14
- successMessage = "You’re on the list!",
15
- note,
16
- onSubmit,
17
- ...props
18
- }: InlineProps) {
19
- const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
20
-
21
- return (
22
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
23
- {done ? (
24
- <NewsletterSuccess message={successMessage} />
25
- ) : (
26
- <>
27
- <label htmlFor="newsletter-email" className="sr-only">
28
- Your email address
29
- </label>
30
- <div className="flex flex-col gap-3 sm:flex-row">
31
- <input
32
- id="newsletter-email"
33
- type="email"
34
- autoComplete="email"
35
- value={email}
36
- onChange={(e) => updateEmail(e.target.value)}
37
- placeholder={placeholder}
38
- className={cn(inputBase, "h-11")}
39
- />
40
- <button
41
- type="submit"
42
- 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"
43
- >
44
- {buttonLabel}
45
- <ArrowRight className="h-4 w-4 opacity-60" />
46
- </button>
47
- </div>
48
- <NewsletterNote error={error} note={note} />
49
- </>
50
- )}
51
- </form>
52
- );
53
- }
54
- NewsletterInline.displayName = "NewsletterInline";
@@ -1,52 +0,0 @@
1
- "use client";
2
- import { cn } from "../../../utils/cn";
3
- import { ArrowRight } from "../../../icons";
4
- import { useNewsletterForm } from "../useNewsletterForm";
5
- import { NewsletterSuccess, NewsletterNote } from "../parts";
6
- import type { NewsletterProps } from "../types";
7
-
8
- interface UnderlineProps extends Omit<NewsletterProps, "option"> {}
9
-
10
- export function NewsletterUnderline({
11
- className,
12
- placeholder = "you@company.com",
13
- buttonLabel = "Join",
14
- successMessage = "You’re on the list!",
15
- note,
16
- onSubmit,
17
- ...props
18
- }: UnderlineProps) {
19
- const { email, done, error, handleSubmit, updateEmail } = useNewsletterForm(onSubmit);
20
-
21
- return (
22
- <form onSubmit={handleSubmit} className={cn("w-full", className)} {...props}>
23
- {done ? (
24
- <NewsletterSuccess message={successMessage} />
25
- ) : (
26
- <div className="flex items-end gap-3 border-b border-foreground/20 pb-3 transition-colors duration-200 focus-within:border-foreground">
27
- <label htmlFor="newsletter-email" className="sr-only">
28
- Your email address
29
- </label>
30
- <input
31
- id="newsletter-email"
32
- type="email"
33
- autoComplete="email"
34
- value={email}
35
- onChange={(e) => updateEmail(e.target.value)}
36
- placeholder={placeholder}
37
- className="w-full bg-transparent text-sm text-foreground placeholder:text-muted-foreground focus:outline-none"
38
- />
39
- <button
40
- type="submit"
41
- 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"
42
- >
43
- {buttonLabel}
44
- <ArrowRight className="h-4 w-4 transition-transform duration-200 group-hover:translate-x-0.5" />
45
- </button>
46
- </div>
47
- )}
48
- <NewsletterNote error={error} note={note} />
49
- </form>
50
- );
51
- }
52
- NewsletterUnderline.displayName = "NewsletterUnderline";
@@ -1,37 +0,0 @@
1
- "use client";
2
- import { useState } from "react";
3
- import type { PricingProps } from "./types";
4
- import { PricingCards } from "./variants/Cards";
5
- import { PricingBento } from "./variants/Bento";
6
- import { PricingCompact } from "./variants/Compact";
7
-
8
- export const Pricing = ({
9
- className,
10
- plans,
11
- option = "cards",
12
- defaultBilling = "monthly",
13
- onSelect,
14
- ...props
15
- }: PricingProps) => {
16
- const [billing, setBilling] = useState<"monthly" | "yearly">(defaultBilling);
17
-
18
- if (option === "compact") {
19
- return <PricingCompact className={className} plans={plans} billing={billing} onSelect={onSelect} {...props} />;
20
- }
21
-
22
- if (option === "bento") {
23
- return <PricingBento className={className} plans={plans} billing={billing} onSelect={onSelect} {...props} />;
24
- }
25
-
26
- return (
27
- <PricingCards
28
- className={className}
29
- plans={plans}
30
- billing={billing}
31
- onSelect={onSelect}
32
- onBillingChange={setBilling}
33
- {...props}
34
- />
35
- );
36
- };
37
- Pricing.displayName = "Pricing";
@@ -1,2 +0,0 @@
1
- export { Pricing } from "./Pricing";
2
- export type { PricingProps, PricingOption, Plan } from "./types";
@@ -1,132 +0,0 @@
1
- "use client";
2
- import { cn } from "../../utils/cn";
3
- import { getContrastText } from "../../utils/contrast";
4
- import { useTokenForeground } from "../../utils/useTokenForeground";
5
- import { ArrowRight } from "../../icons";
6
- import type { Plan } from "./types";
7
-
8
- export function PriceValue({
9
- value,
10
- billing,
11
- dim,
12
- }: {
13
- value: number | null;
14
- billing: "monthly" | "yearly";
15
- /** False when rendered on a colored background where opacity would kill contrast. */
16
- dim?: boolean;
17
- }) {
18
- if (value === null) {
19
- return <span>Custom</span>;
20
- }
21
- return (
22
- <>
23
- <span className="text-[2.6rem] font-semibold tracking-tight sm:text-[3rem]">${value}</span>
24
- <span className={cn("text-sm font-normal", dim && "opacity-60")}>
25
- /{billing === "monthly" ? "mo" : "yr"}
26
- </span>
27
- </>
28
- );
29
- }
30
-
31
- export function BillingToggle({
32
- billing,
33
- onChange,
34
- dark,
35
- }: {
36
- billing: "monthly" | "yearly";
37
- onChange: (b: "monthly" | "yearly") => void;
38
- dark?: boolean;
39
- }) {
40
- // The accent token is a background color, but its shipped "on-accent"
41
- // pairing isn't guaranteed to be contrast-safe (e.g. white on amber).
42
- // Derive the badge text color from the actual accent value instead.
43
- const accentText = useTokenForeground("--color-accent");
44
- return (
45
- <div className="mb-10 flex items-center justify-center gap-4">
46
- <span
47
- className={cn(
48
- "text-sm font-medium transition-colors",
49
- billing === "monthly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
50
- )}
51
- >
52
- Monthly
53
- </span>
54
- <button
55
- type="button"
56
- role="switch"
57
- aria-checked={billing === "yearly"}
58
- aria-label="Billing period"
59
- onClick={() => onChange(billing === "monthly" ? "yearly" : "monthly")}
60
- className={cn(
61
- "relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200",
62
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
63
- billing === "yearly" ? "bg-primary" : "bg-border",
64
- )}
65
- >
66
- <span
67
- className={cn(
68
- "inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform duration-200",
69
- billing === "yearly" ? "translate-x-6" : "translate-x-1",
70
- )}
71
- />
72
- </button>
73
- <span
74
- className={cn(
75
- "flex items-center gap-2 text-sm font-medium transition-colors",
76
- billing === "yearly" ? (dark ? "text-white" : "text-foreground") : "text-muted-foreground",
77
- )}
78
- >
79
- Yearly
80
- <span
81
- className="rounded-full bg-accent px-2 py-0.5 text-xs font-semibold"
82
- style={{ color: accentText }}
83
- >
84
- −2 ay
85
- </span>
86
- </span>
87
- </div>
88
- );
89
- }
90
-
91
- export function PlanButton({
92
- plan,
93
- billing,
94
- onSelect,
95
- dark,
96
- }: {
97
- plan: Plan;
98
- billing: "monthly" | "yearly";
99
- onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
100
- dark?: boolean;
101
- }) {
102
- const isHighlighted = plan.highlighted && !plan.customColor;
103
- const fg = plan.customColor ? getContrastText(plan.customColor, "#111111", "#ffffff") : undefined;
104
- return (
105
- <button
106
- type="button"
107
- onClick={() => onSelect?.(plan, billing)}
108
- className={cn(
109
- "inline-flex h-11 cursor-pointer items-center justify-center gap-2 rounded-lg px-5 text-sm font-semibold transition-colors duration-200",
110
- "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
111
- isHighlighted || plan.customColor
112
- ? "hover:brightness-95"
113
- : dark
114
- ? "bg-white text-black hover:bg-white/90"
115
- : "border border-border text-foreground hover:bg-surface",
116
- )}
117
- style={
118
- plan.customColor
119
- ? { backgroundColor: plan.customColor, color: fg }
120
- : isHighlighted
121
- ? {
122
- backgroundColor: "rgb(var(--color-primary))",
123
- color: "rgb(var(--color-on-primary))",
124
- }
125
- : undefined
126
- }
127
- >
128
- {plan.cta || "Start"}
129
- <ArrowRight className="h-4 w-4 opacity-60" />
130
- </button>
131
- );
132
- }
@@ -1,28 +0,0 @@
1
- import type { HTMLAttributes } from "react";
2
-
3
- export type PricingOption = "cards" | "bento" | "compact";
4
-
5
- export interface Plan {
6
- name: string;
7
- description?: string;
8
- monthly: number | null;
9
- yearly: number | null;
10
- features: string[];
11
- cta?: string;
12
- href?: string;
13
- highlighted?: boolean;
14
- customColor?: string;
15
- }
16
-
17
- export interface PricingProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
18
- plans: Plan[];
19
- option?: PricingOption;
20
- defaultBilling?: "monthly" | "yearly";
21
- onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
22
- }
23
-
24
- export interface PricingVariantProps extends Omit<HTMLAttributes<HTMLDivElement>, "onSelect"> {
25
- plans: Plan[];
26
- billing: "monthly" | "yearly";
27
- onSelect?: (plan: Plan, billing: "monthly" | "yearly") => void;
28
- }