@ugurdemirel/landcraft 0.1.5 → 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.
- package/package.json +4 -4
- package/src/components/Badge.test.tsx +0 -55
- package/src/components/Badge.tsx +0 -68
- package/src/components/Blog/Blog.test.tsx +0 -64
- package/src/components/Blog/BlogCard.tsx +0 -12
- package/src/components/Blog/BlogSection.tsx +0 -95
- package/src/components/Blog/index.ts +0 -8
- package/src/components/Blog/parts.tsx +0 -38
- package/src/components/Blog/types.ts +0 -42
- package/src/components/Blog/variants/Card.tsx +0 -62
- package/src/components/Blog/variants/Row.tsx +0 -41
- package/src/components/Button.test.tsx +0 -91
- package/src/components/Button.tsx +0 -113
- package/src/components/CTA/CTA.test.tsx +0 -53
- package/src/components/CTA/CTA.tsx +0 -12
- package/src/components/CTA/index.ts +0 -2
- package/src/components/CTA/parts.tsx +0 -26
- package/src/components/CTA/types.ts +0 -12
- package/src/components/CTA/variants/Inverse.tsx +0 -50
- package/src/components/CTA/variants/Panel.tsx +0 -60
- package/src/components/CTA/variants/Surface.tsx +0 -34
- package/src/components/Card.test.tsx +0 -49
- package/src/components/Card.tsx +0 -80
- package/src/components/Container.test.tsx +0 -52
- package/src/components/Container.tsx +0 -86
- package/src/components/FAQ/FAQ.test.tsx +0 -54
- package/src/components/FAQ/FAQ.tsx +0 -48
- package/src/components/FAQ/index.ts +0 -2
- package/src/components/FAQ/parts.tsx +0 -74
- package/src/components/FAQ/types.ts +0 -18
- package/src/components/FAQ/variants/Accordion.tsx +0 -46
- package/src/components/FAQ/variants/Cards.tsx +0 -40
- package/src/components/FAQ/variants/Split.tsx +0 -44
- package/src/components/Features/FeatureCard.tsx +0 -44
- package/src/components/Features/FeatureGrid.tsx +0 -14
- package/src/components/Features/Features.test.tsx +0 -49
- package/src/components/Features/index.ts +0 -3
- package/src/components/Features/parts.ts +0 -15
- package/src/components/Features/types.ts +0 -25
- package/src/components/Features/variants/Bento.tsx +0 -32
- package/src/components/Features/variants/Columns.tsx +0 -21
- package/src/components/Features/variants/EditorialRows.tsx +0 -38
- package/src/components/Footer/Footer.test.tsx +0 -49
- package/src/components/Footer/Footer.tsx +0 -11
- package/src/components/Footer/index.ts +0 -2
- package/src/components/Footer/parts.tsx +0 -101
- package/src/components/Footer/types.ts +0 -32
- package/src/components/Footer/variants/Classic.tsx +0 -59
- package/src/components/Footer/variants/Editorial.tsx +0 -47
- package/src/components/Footer/variants/Minimal.tsx +0 -33
- package/src/components/Hero/Hero.test.tsx +0 -71
- package/src/components/Hero/Hero.tsx +0 -12
- package/src/components/Hero/index.ts +0 -2
- package/src/components/Hero/parts.tsx +0 -29
- package/src/components/Hero/types.ts +0 -20
- package/src/components/Hero/variants/Centered.tsx +0 -54
- package/src/components/Hero/variants/Split.tsx +0 -53
- package/src/components/Hero/variants/Statement.tsx +0 -60
- package/src/components/LanguageSwitcher/LanguageSwitcher.test.tsx +0 -64
- package/src/components/LanguageSwitcher/LanguageSwitcher.tsx +0 -37
- package/src/components/LanguageSwitcher/index.ts +0 -6
- package/src/components/LanguageSwitcher/parts.tsx +0 -144
- package/src/components/LanguageSwitcher/types.ts +0 -40
- package/src/components/LanguageSwitcher/useLanguageSwitcher.ts +0 -23
- package/src/components/LanguageSwitcher/variants/Dropdown.tsx +0 -88
- package/src/components/LanguageSwitcher/variants/Modal.tsx +0 -56
- package/src/components/LogoCloud/LogoCloud.test.tsx +0 -35
- package/src/components/LogoCloud/LogoCloud.tsx +0 -14
- package/src/components/LogoCloud/index.ts +0 -2
- package/src/components/LogoCloud/parts.tsx +0 -32
- package/src/components/LogoCloud/types.ts +0 -16
- package/src/components/LogoCloud/variants/Marquee.tsx +0 -30
- package/src/components/LogoCloud/variants/Quiet.tsx +0 -26
- package/src/components/LogoCloud/variants/Strip.tsx +0 -25
- package/src/components/MegaMenu/MegaMenu.test.tsx +0 -89
- package/src/components/MegaMenu/MegaMenu.tsx +0 -454
- package/src/components/MegaMenu/index.ts +0 -9
- package/src/components/MegaMenu/types.ts +0 -56
- package/src/components/Modal.test.tsx +0 -80
- package/src/components/Modal.tsx +0 -125
- package/src/components/Navbar/Navbar.test.tsx +0 -91
- package/src/components/Navbar/Navbar.tsx +0 -185
- package/src/components/Navbar/index.ts +0 -2
- package/src/components/Navbar/types.ts +0 -30
- package/src/components/Newsletter/Newsletter.test.tsx +0 -45
- package/src/components/Newsletter/Newsletter.tsx +0 -12
- package/src/components/Newsletter/index.ts +0 -2
- package/src/components/Newsletter/parts.tsx +0 -23
- package/src/components/Newsletter/types.ts +0 -12
- package/src/components/Newsletter/useNewsletterForm.ts +0 -27
- package/src/components/Newsletter/variants/Card.tsx +0 -62
- package/src/components/Newsletter/variants/Inline.tsx +0 -54
- package/src/components/Newsletter/variants/Underline.tsx +0 -52
- package/src/components/Pricing/Pricing.test.tsx +0 -67
- package/src/components/Pricing/Pricing.tsx +0 -37
- package/src/components/Pricing/index.ts +0 -2
- package/src/components/Pricing/parts.tsx +0 -132
- package/src/components/Pricing/types.ts +0 -28
- package/src/components/Pricing/variants/Bento.tsx +0 -72
- package/src/components/Pricing/variants/Cards.tsx +0 -79
- package/src/components/Pricing/variants/Compact.tsx +0 -69
- package/src/components/Prose.test.tsx +0 -49
- package/src/components/Prose.tsx +0 -55
- package/src/components/Section.test.tsx +0 -51
- package/src/components/Section.tsx +0 -106
- package/src/components/Slot.test.tsx +0 -45
- package/src/components/Slot.tsx +0 -78
- package/src/components/Stats/Stats.test.tsx +0 -47
- package/src/components/Stats/Stats.tsx +0 -16
- package/src/components/Stats/index.ts +0 -2
- package/src/components/Stats/parts.tsx +0 -29
- package/src/components/Stats/types.ts +0 -23
- package/src/components/Stats/variants/Cells.tsx +0 -34
- package/src/components/Stats/variants/Editorial.tsx +0 -41
- package/src/components/Stats/variants/Hairline.tsx +0 -31
- package/src/components/Stats/variants/Ticker.tsx +0 -36
- package/src/components/Testimonials/TestimonialCard.tsx +0 -61
- package/src/components/Testimonials/Testimonials.test.tsx +0 -48
- package/src/components/Testimonials/Testimonials.tsx +0 -14
- package/src/components/Testimonials/index.ts +0 -3
- package/src/components/Testimonials/parts.ts +0 -7
- package/src/components/Testimonials/types.ts +0 -24
- package/src/components/Testimonials/variants/Carousel.tsx +0 -94
- package/src/components/Testimonials/variants/Grid.tsx +0 -19
- package/src/components/Testimonials/variants/Marquee.tsx +0 -28
- package/src/icons.tsx +0 -290
- package/src/index.ts +0 -120
- package/src/styles/index.css +0 -18
- package/src/test/setup.ts +0 -7
- package/src/utils/cn.test.ts +0 -21
- package/src/utils/cn.ts +0 -6
- package/src/utils/contrast.test.ts +0 -71
- package/src/utils/contrast.ts +0 -107
- package/src/utils/iso2Flag.test.ts +0 -26
- package/src/utils/iso2Flag.ts +0 -26
- package/src/utils/useTokenForeground.test.ts +0 -50
- package/src/utils/useTokenForeground.ts +0 -39
- /package/{src/styles → dist}/theme.css +0 -0
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import type { ElementType, ReactNode } from "react";
|
|
2
|
-
import { cn } from "../../utils/cn";
|
|
3
|
-
import type { FooterColumn } from "./types";
|
|
4
|
-
|
|
5
|
-
export function FooterLogoNode({
|
|
6
|
-
logo,
|
|
7
|
-
logoSrc,
|
|
8
|
-
logoAlt,
|
|
9
|
-
logoClassName,
|
|
10
|
-
brand,
|
|
11
|
-
imageClassName = "h-7 w-auto",
|
|
12
|
-
}: {
|
|
13
|
-
logo?: ReactNode;
|
|
14
|
-
logoSrc?: string;
|
|
15
|
-
logoAlt?: string;
|
|
16
|
-
logoClassName?: string;
|
|
17
|
-
brand?: ReactNode;
|
|
18
|
-
imageClassName?: string;
|
|
19
|
-
}) {
|
|
20
|
-
if (logo) return logo;
|
|
21
|
-
if (logoSrc) {
|
|
22
|
-
return (
|
|
23
|
-
<img
|
|
24
|
-
src={logoSrc}
|
|
25
|
-
alt={logoAlt ?? (typeof brand === "string" ? brand : "Logo")}
|
|
26
|
-
className={cn(imageClassName, logoClassName)}
|
|
27
|
-
/>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function FooterWordmark({ brand }: { brand?: ReactNode }) {
|
|
34
|
-
return (
|
|
35
|
-
<span className="flex items-center gap-2 font-display text-lg font-bold tracking-tight">
|
|
36
|
-
<span aria-hidden className="inline-block h-2.5 w-2.5 rounded-sm bg-current" />
|
|
37
|
-
{brand}
|
|
38
|
-
</span>
|
|
39
|
-
);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function FooterEditorialWordmark({ brand }: { brand?: ReactNode }) {
|
|
43
|
-
return (
|
|
44
|
-
<span className="font-display text-5xl font-bold tracking-[-0.03em] text-foreground sm:text-7xl">
|
|
45
|
-
{brand}
|
|
46
|
-
</span>
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function FooterColumnsBlock({
|
|
51
|
-
columns,
|
|
52
|
-
Link,
|
|
53
|
-
linkClassName,
|
|
54
|
-
}: {
|
|
55
|
-
columns: FooterColumn[];
|
|
56
|
-
Link: ElementType;
|
|
57
|
-
linkClassName: string;
|
|
58
|
-
}) {
|
|
59
|
-
return (
|
|
60
|
-
<div className="grid grid-cols-2 gap-8 md:grid-cols-4">
|
|
61
|
-
{columns.map((column) => (
|
|
62
|
-
<div key={column.title}>
|
|
63
|
-
<h3 className="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
64
|
-
{column.title}
|
|
65
|
-
</h3>
|
|
66
|
-
<ul className="mt-4 space-y-2.5">
|
|
67
|
-
{column.links.map((link) => (
|
|
68
|
-
<li key={link.href}>
|
|
69
|
-
<Link href={link.href} className={linkClassName}>
|
|
70
|
-
{link.label}
|
|
71
|
-
</Link>
|
|
72
|
-
</li>
|
|
73
|
-
))}
|
|
74
|
-
</ul>
|
|
75
|
-
</div>
|
|
76
|
-
))}
|
|
77
|
-
</div>
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function FooterBottomBar({
|
|
82
|
-
bottom,
|
|
83
|
-
languageSwitcher,
|
|
84
|
-
brand,
|
|
85
|
-
wrapperClassName,
|
|
86
|
-
textClassName,
|
|
87
|
-
}: {
|
|
88
|
-
bottom?: ReactNode;
|
|
89
|
-
languageSwitcher?: ReactNode;
|
|
90
|
-
brand?: ReactNode;
|
|
91
|
-
wrapperClassName: string;
|
|
92
|
-
textClassName: string;
|
|
93
|
-
}) {
|
|
94
|
-
return (
|
|
95
|
-
<div className={wrapperClassName}>
|
|
96
|
-
<span className={cn(textClassName)}>{bottom}</span>
|
|
97
|
-
{languageSwitcher ? <div>{languageSwitcher}</div> : null}
|
|
98
|
-
<span className={cn(textClassName)}>© {new Date().getFullYear()} {brand}. All rights reserved.</span>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { ElementType, HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export interface FooterLink {
|
|
4
|
-
label: string;
|
|
5
|
-
href: string;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface FooterColumn {
|
|
9
|
-
title: string;
|
|
10
|
-
links: FooterLink[];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface FooterProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
14
|
-
brand?: ReactNode;
|
|
15
|
-
/** Custom logo node (component, <img>, inline SVG…). Replaces the wordmark. */
|
|
16
|
-
logo?: ReactNode;
|
|
17
|
-
/** Quick logo image source. */
|
|
18
|
-
logoSrc?: string;
|
|
19
|
-
logoAlt?: string;
|
|
20
|
-
/** Size / position tuning for logo images. Defaults to `h-7 w-auto`. */
|
|
21
|
-
logoClassName?: string;
|
|
22
|
-
description?: string;
|
|
23
|
-
columns: FooterColumn[];
|
|
24
|
-
socials?: ReactNode;
|
|
25
|
-
bottom?: ReactNode;
|
|
26
|
-
/** Optional language switcher rendered in the footer's bottom bar. */
|
|
27
|
-
languageSwitcher?: ReactNode;
|
|
28
|
-
option?: "classic" | "minimal" | "editorial";
|
|
29
|
-
badge?: ReactNode;
|
|
30
|
-
/** Component used to render footer links. Defaults to `<a>`. Pass your router's `<Link>` (Next.js, Remix, React Router…) for framework-aware navigation. */
|
|
31
|
-
LinkComponent?: ElementType;
|
|
32
|
-
}
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { Container } from "../../Container";
|
|
3
|
-
import type { FooterProps } from "../types";
|
|
4
|
-
import {
|
|
5
|
-
FooterWordmark,
|
|
6
|
-
FooterLogoNode,
|
|
7
|
-
FooterBottomBar,
|
|
8
|
-
} from "../parts";
|
|
9
|
-
|
|
10
|
-
interface ClassicProps extends FooterProps {}
|
|
11
|
-
|
|
12
|
-
export function FooterClassic({ className, brand, logo, logoSrc, logoAlt, logoClassName, description, columns, socials, bottom, languageSwitcher, badge, LinkComponent, ...props }: ClassicProps) {
|
|
13
|
-
const Link = LinkComponent ?? "a";
|
|
14
|
-
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand }) ?? <FooterWordmark brand={brand} />;
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<footer className={cn("w-full bg-secondary text-on-secondary", className)} {...props}>
|
|
18
|
-
<Container className="py-16">
|
|
19
|
-
<div className="grid grid-cols-2 gap-10 md:grid-cols-4 lg:grid-cols-6">
|
|
20
|
-
<div className="col-span-2">
|
|
21
|
-
<span style={{ color: "rgb(var(--color-on-secondary))" }}>{brandNode}</span>
|
|
22
|
-
{description ? (
|
|
23
|
-
<p className="mt-4 max-w-xs text-sm leading-6 text-on-secondary/60">{description}</p>
|
|
24
|
-
) : null}
|
|
25
|
-
{socials ? <div className="mt-6 flex items-center gap-3">{socials}</div> : null}
|
|
26
|
-
{badge ? <div className="mt-6">{badge}</div> : null}
|
|
27
|
-
</div>
|
|
28
|
-
{columns.map((column) => (
|
|
29
|
-
<div key={column.title}>
|
|
30
|
-
<h3 className="text-xs font-semibold uppercase tracking-[0.16em] text-on-secondary/50">
|
|
31
|
-
{column.title}
|
|
32
|
-
</h3>
|
|
33
|
-
<ul className="mt-4 space-y-2.5">
|
|
34
|
-
{column.links.map((link) => (
|
|
35
|
-
<li key={link.href}>
|
|
36
|
-
<Link
|
|
37
|
-
href={link.href}
|
|
38
|
-
className="text-sm text-on-secondary/70 transition-colors duration-150 hover:text-on-secondary"
|
|
39
|
-
>
|
|
40
|
-
{link.label}
|
|
41
|
-
</Link>
|
|
42
|
-
</li>
|
|
43
|
-
))}
|
|
44
|
-
</ul>
|
|
45
|
-
</div>
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
<FooterBottomBar
|
|
49
|
-
bottom={bottom}
|
|
50
|
-
languageSwitcher={languageSwitcher}
|
|
51
|
-
brand={brand}
|
|
52
|
-
wrapperClassName="mt-14 flex flex-col items-center justify-between gap-4 border-t border-white/10 pt-8 text-sm text-on-secondary/50 sm:flex-row"
|
|
53
|
-
textClassName=""
|
|
54
|
-
/>
|
|
55
|
-
</Container>
|
|
56
|
-
</footer>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
FooterClassic.displayName = "FooterClassic";
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { Container } from "../../Container";
|
|
3
|
-
import type { FooterProps } from "../types";
|
|
4
|
-
import {
|
|
5
|
-
FooterColumnsBlock,
|
|
6
|
-
FooterEditorialWordmark,
|
|
7
|
-
FooterLogoNode,
|
|
8
|
-
FooterBottomBar,
|
|
9
|
-
} from "../parts";
|
|
10
|
-
|
|
11
|
-
interface EditorialProps extends FooterProps {}
|
|
12
|
-
|
|
13
|
-
export function FooterEditorial({ className, brand, logo, logoSrc, logoAlt, logoClassName, description, columns, socials, bottom, languageSwitcher, LinkComponent, ...props }: EditorialProps) {
|
|
14
|
-
const Link = LinkComponent ?? "a";
|
|
15
|
-
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand, imageClassName: "h-12 w-auto" }) ?? <FooterEditorialWordmark brand={brand} />;
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<footer className={cn("w-full border-t border-border bg-background", className)} {...props}>
|
|
19
|
-
<Container className="py-16 sm:py-20">
|
|
20
|
-
<div className="mb-14 flex flex-col justify-between gap-8 sm:flex-row sm:items-end">
|
|
21
|
-
<div>
|
|
22
|
-
{brandNode}
|
|
23
|
-
{description ? (
|
|
24
|
-
<p className="mt-3 max-w-sm text-sm leading-6 text-muted-foreground">{description}</p>
|
|
25
|
-
) : null}
|
|
26
|
-
</div>
|
|
27
|
-
{socials ? <div className="flex items-center gap-3">{socials}</div> : null}
|
|
28
|
-
</div>
|
|
29
|
-
<div className="border-t border-border pt-12">
|
|
30
|
-
<FooterColumnsBlock
|
|
31
|
-
columns={columns}
|
|
32
|
-
Link={Link}
|
|
33
|
-
linkClassName="text-[15px] text-foreground/80 transition-colors duration-150 hover:text-foreground"
|
|
34
|
-
/>
|
|
35
|
-
</div>
|
|
36
|
-
<FooterBottomBar
|
|
37
|
-
bottom={bottom}
|
|
38
|
-
languageSwitcher={languageSwitcher}
|
|
39
|
-
brand={brand}
|
|
40
|
-
wrapperClassName="mt-12 flex flex-col items-center justify-between gap-4 border-t border-border pt-8 text-sm text-muted-foreground sm:flex-row"
|
|
41
|
-
textClassName=""
|
|
42
|
-
/>
|
|
43
|
-
</Container>
|
|
44
|
-
</footer>
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
FooterEditorial.displayName = "FooterEditorial";
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../../utils/cn";
|
|
2
|
-
import { Container } from "../../Container";
|
|
3
|
-
import type { FooterProps } from "../types";
|
|
4
|
-
import { FooterWordmark, FooterLogoNode } from "../parts";
|
|
5
|
-
|
|
6
|
-
interface MinimalProps extends FooterProps {}
|
|
7
|
-
|
|
8
|
-
export function FooterMinimal({ className, brand, logo, logoSrc, logoAlt, logoClassName, columns, languageSwitcher, bottom, LinkComponent, ...props }: MinimalProps) {
|
|
9
|
-
const Link = LinkComponent ?? "a";
|
|
10
|
-
const brandNode = FooterLogoNode({ logo, logoSrc, logoAlt, logoClassName, brand }) ?? <FooterWordmark brand={brand} />;
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<footer className={cn("w-full border-t border-border bg-background", className)} {...props}>
|
|
14
|
-
<Container className="flex flex-col items-center justify-between gap-6 py-10 sm:flex-row">
|
|
15
|
-
{brandNode}
|
|
16
|
-
<ul className="flex flex-wrap items-center justify-center gap-x-8 gap-y-3">
|
|
17
|
-
{columns.flatMap((c) => c.links).slice(0, 6).map((link) => (
|
|
18
|
-
<li key={link.href}>
|
|
19
|
-
<Link href={link.href} className="text-sm text-muted-foreground transition-colors duration-150 hover:text-foreground">
|
|
20
|
-
{link.label}
|
|
21
|
-
</Link>
|
|
22
|
-
</li>
|
|
23
|
-
))}
|
|
24
|
-
</ul>
|
|
25
|
-
<div className="flex flex-col items-center gap-3">
|
|
26
|
-
{languageSwitcher ? <div>{languageSwitcher}</div> : null}
|
|
27
|
-
<div className="text-sm text-muted-foreground">{bottom}</div>
|
|
28
|
-
</div>
|
|
29
|
-
</Container>
|
|
30
|
-
</footer>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
FooterMinimal.displayName = "FooterMinimal";
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Hero } from "./Hero";
|
|
4
|
-
|
|
5
|
-
const base = {
|
|
6
|
-
title: "Build something great",
|
|
7
|
-
description: "A short description.",
|
|
8
|
-
eyebrow: "New release",
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
describe("Hero", () => {
|
|
12
|
-
it("renders the headline and description", () => {
|
|
13
|
-
render(<Hero {...base} />);
|
|
14
|
-
expect(
|
|
15
|
-
screen.getByRole("heading", { name: "Build something great" }),
|
|
16
|
-
).toBeInTheDocument();
|
|
17
|
-
expect(screen.getByText("A short description.")).toBeInTheDocument();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
it("renders an eyebrow, primary and secondary actions", () => {
|
|
21
|
-
render(
|
|
22
|
-
<Hero
|
|
23
|
-
{...base}
|
|
24
|
-
primaryAction={<a href="#a">Primary</a>}
|
|
25
|
-
secondaryAction={<a href="#b">Secondary</a>}
|
|
26
|
-
/>,
|
|
27
|
-
);
|
|
28
|
-
expect(screen.getByText("New release")).toBeInTheDocument();
|
|
29
|
-
expect(screen.getByRole("link", { name: "Primary" })).toBeInTheDocument();
|
|
30
|
-
expect(screen.getByRole("link", { name: "Secondary" })).toBeInTheDocument();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("renders the split variant by default with meta row and media", () => {
|
|
34
|
-
render(
|
|
35
|
-
<Hero
|
|
36
|
-
{...base}
|
|
37
|
-
media={<div data-testid="media" />}
|
|
38
|
-
meta={[{ label: "4.9/5", icon: <span>★</span> }]}
|
|
39
|
-
/>,
|
|
40
|
-
);
|
|
41
|
-
expect(screen.getByTestId("media")).toBeInTheDocument();
|
|
42
|
-
expect(screen.getByText("4.9/5")).toBeInTheDocument();
|
|
43
|
-
const section = screen.getByTestId("media").closest("section");
|
|
44
|
-
expect(section).toHaveClass("bg-background");
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it("renders the centered variant with centered copy", () => {
|
|
48
|
-
render(<Hero {...base} variant="centered" media={<div data-testid="m" />} />);
|
|
49
|
-
const h1 = screen.getByRole("heading", { name: "Build something great" });
|
|
50
|
-
expect(h1).toBeInTheDocument();
|
|
51
|
-
expect(screen.getByTestId("m")).toBeInTheDocument();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("renders the statement variant as an ink band with a grain overlay", () => {
|
|
55
|
-
const { container } = render(<Hero {...base} variant="statement" />);
|
|
56
|
-
const section = container.querySelector("section");
|
|
57
|
-
expect(section).toHaveClass("bg-secondary");
|
|
58
|
-
// Statement paints a grain overlay div.
|
|
59
|
-
expect(container.querySelector(".grain")).toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("omits the meta row when no meta is supplied", () => {
|
|
63
|
-
render(<Hero {...base} />);
|
|
64
|
-
expect(screen.queryByRole("list")).not.toBeInTheDocument();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("forwards an id to the section", () => {
|
|
68
|
-
const { container } = render(<Hero {...base} id="home" />);
|
|
69
|
-
expect(container.querySelector("section#home")).not.toBeNull();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { HeroProps } from "./types";
|
|
3
|
-
import { HeroSplit } from "./variants/Split";
|
|
4
|
-
import { HeroCentered } from "./variants/Centered";
|
|
5
|
-
import { HeroStatement } from "./variants/Statement";
|
|
6
|
-
|
|
7
|
-
export const Hero = forwardRef<HTMLElement, HeroProps>(({ variant = "split", ...props }, ref) => {
|
|
8
|
-
if (variant === "centered") return <HeroCentered ref={ref} {...props} />;
|
|
9
|
-
if (variant === "statement") return <HeroStatement ref={ref} {...props} />;
|
|
10
|
-
return <HeroSplit ref={ref} {...props} />;
|
|
11
|
-
});
|
|
12
|
-
Hero.displayName = "Hero";
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export const heroContainer =
|
|
4
|
-
"relative mx-auto flex w-full max-w-6xl flex-col items-center px-5 sm:px-8";
|
|
5
|
-
|
|
6
|
-
export const heroTitleBase =
|
|
7
|
-
"font-display font-semibold tracking-[-0.03em] text-balance text-foreground";
|
|
8
|
-
|
|
9
|
-
export function HeroEyebrow({ eyebrow }: { eyebrow?: ReactNode }) {
|
|
10
|
-
if (typeof eyebrow === "string") {
|
|
11
|
-
return (
|
|
12
|
-
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-primary">
|
|
13
|
-
{eyebrow}
|
|
14
|
-
</p>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
return eyebrow ? <div>{eyebrow}</div> : null;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function HeroActions({
|
|
21
|
-
primaryAction,
|
|
22
|
-
secondaryAction,
|
|
23
|
-
}: {
|
|
24
|
-
primaryAction?: ReactNode;
|
|
25
|
-
secondaryAction?: ReactNode;
|
|
26
|
-
}) {
|
|
27
|
-
if (!primaryAction && !secondaryAction) return null;
|
|
28
|
-
return <div className="flex flex-wrap items-center gap-4">{primaryAction}{secondaryAction}</div>;
|
|
29
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export type HeroVariant = "split" | "centered" | "statement";
|
|
4
|
-
|
|
5
|
-
export interface HeroMeta {
|
|
6
|
-
label: string;
|
|
7
|
-
icon?: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface HeroProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
11
|
-
variant?: HeroVariant;
|
|
12
|
-
eyebrow?: ReactNode;
|
|
13
|
-
title: ReactNode;
|
|
14
|
-
description?: ReactNode;
|
|
15
|
-
primaryAction?: ReactNode;
|
|
16
|
-
secondaryAction?: ReactNode;
|
|
17
|
-
media?: ReactNode;
|
|
18
|
-
/** Small trust/status row shown under the copy or CTAs. */
|
|
19
|
-
meta?: HeroMeta[];
|
|
20
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { HeroActions, HeroEyebrow, heroContainer, heroTitleBase } from "../parts";
|
|
4
|
-
import type { HeroProps } from "../types";
|
|
5
|
-
|
|
6
|
-
/** Centered — understated, one statement, generous white space. */
|
|
7
|
-
export const HeroCentered = forwardRef<HTMLElement, HeroProps>(
|
|
8
|
-
(
|
|
9
|
-
{ className, eyebrow, title, description, primaryAction, secondaryAction, media, meta, id, ...props },
|
|
10
|
-
ref,
|
|
11
|
-
) => (
|
|
12
|
-
<section
|
|
13
|
-
ref={ref}
|
|
14
|
-
id={id}
|
|
15
|
-
className={cn("relative w-full border-b border-border bg-background", className)}
|
|
16
|
-
{...props}
|
|
17
|
-
>
|
|
18
|
-
<div className={cn(heroContainer, "pt-24 pb-20 text-center sm:pt-32 sm:pb-24")}>
|
|
19
|
-
<HeroEyebrow eyebrow={eyebrow} />
|
|
20
|
-
<h1 className={cn(heroTitleBase, "mt-7 max-w-4xl text-5xl leading-[1.04] sm:text-6xl lg:text-7xl")}>
|
|
21
|
-
{title}
|
|
22
|
-
</h1>
|
|
23
|
-
{description ? (
|
|
24
|
-
<p className="mx-auto mt-7 max-w-xl text-pretty leading-relaxed text-muted-foreground sm:text-lg">
|
|
25
|
-
{description}
|
|
26
|
-
</p>
|
|
27
|
-
) : null}
|
|
28
|
-
{primaryAction || secondaryAction ? (
|
|
29
|
-
<div className="mt-10 justify-center">
|
|
30
|
-
<HeroActions primaryAction={primaryAction} secondaryAction={secondaryAction} />
|
|
31
|
-
</div>
|
|
32
|
-
) : null}
|
|
33
|
-
{meta ? (
|
|
34
|
-
<ul className="mt-14 flex flex-wrap items-center justify-center gap-x-9 gap-y-3">
|
|
35
|
-
{meta.map((m, i) => (
|
|
36
|
-
<li
|
|
37
|
-
key={m.label}
|
|
38
|
-
className={cn(
|
|
39
|
-
"flex items-center gap-2 text-sm text-muted-foreground",
|
|
40
|
-
i > 0 && "sm:border-l sm:border-border sm:pl-9",
|
|
41
|
-
)}
|
|
42
|
-
>
|
|
43
|
-
{m.icon ? <span className="text-foreground/35">{m.icon}</span> : null}
|
|
44
|
-
{m.label}
|
|
45
|
-
</li>
|
|
46
|
-
))}
|
|
47
|
-
</ul>
|
|
48
|
-
) : null}
|
|
49
|
-
{media ? <div className="mt-16 w-full">{media}</div> : null}
|
|
50
|
-
</div>
|
|
51
|
-
</section>
|
|
52
|
-
),
|
|
53
|
-
);
|
|
54
|
-
HeroCentered.displayName = "HeroCentered";
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { HeroActions, HeroEyebrow, heroContainer, heroTitleBase } from "../parts";
|
|
4
|
-
import type { HeroProps } from "../types";
|
|
5
|
-
|
|
6
|
-
/** Split — editorial grid: huge type left, media right, meta rule under. */
|
|
7
|
-
export const HeroSplit = forwardRef<HTMLElement, HeroProps>(
|
|
8
|
-
(
|
|
9
|
-
{ className, eyebrow, title, description, primaryAction, secondaryAction, media, meta, id, ...props },
|
|
10
|
-
ref,
|
|
11
|
-
) => (
|
|
12
|
-
<section
|
|
13
|
-
ref={ref}
|
|
14
|
-
id={id}
|
|
15
|
-
className={cn("relative w-full border-b border-border bg-background", className)}
|
|
16
|
-
{...props}
|
|
17
|
-
>
|
|
18
|
-
<div className={cn(
|
|
19
|
-
heroContainer,
|
|
20
|
-
"items-start gap-14 pt-20 pb-20 sm:pt-28 sm:pb-28 lg:grid lg:grid-cols-[1.05fr_0.95fr] lg:gap-20",
|
|
21
|
-
)}>
|
|
22
|
-
<div className="max-w-xl">
|
|
23
|
-
<HeroEyebrow eyebrow={eyebrow} />
|
|
24
|
-
<h1 className={cn(heroTitleBase, "mt-6 text-5xl leading-[1.03] sm:text-6xl lg:text-7xl")}>
|
|
25
|
-
{title}
|
|
26
|
-
</h1>
|
|
27
|
-
{description ? (
|
|
28
|
-
<p className="mt-7 max-w-md text-pretty leading-relaxed text-muted-foreground sm:text-lg">
|
|
29
|
-
{description}
|
|
30
|
-
</p>
|
|
31
|
-
) : null}
|
|
32
|
-
{primaryAction || secondaryAction ? (
|
|
33
|
-
<div className="mt-10">
|
|
34
|
-
<HeroActions primaryAction={primaryAction} secondaryAction={secondaryAction} />
|
|
35
|
-
</div>
|
|
36
|
-
) : null}
|
|
37
|
-
{meta ? (
|
|
38
|
-
<ul className="mt-14 flex flex-wrap items-center gap-x-8 gap-y-3 border-t border-border pt-6">
|
|
39
|
-
{meta.map((m) => (
|
|
40
|
-
<li key={m.label} className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
41
|
-
{m.icon ? <span className="text-foreground/35">{m.icon}</span> : null}
|
|
42
|
-
{m.label}
|
|
43
|
-
</li>
|
|
44
|
-
))}
|
|
45
|
-
</ul>
|
|
46
|
-
) : null}
|
|
47
|
-
</div>
|
|
48
|
-
{media ? <div className="w-full self-center">{media}</div> : null}
|
|
49
|
-
</div>
|
|
50
|
-
</section>
|
|
51
|
-
),
|
|
52
|
-
);
|
|
53
|
-
HeroSplit.displayName = "HeroSplit";
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { forwardRef } from "react";
|
|
3
|
-
import { cn } from "../../../utils/cn";
|
|
4
|
-
import { useTokenForeground } from "../../../utils/useTokenForeground";
|
|
5
|
-
import { HeroActions, heroContainer } from "../parts";
|
|
6
|
-
import type { HeroProps } from "../types";
|
|
7
|
-
|
|
8
|
-
/** Statement — full-bleed ink band. No eyebrow, pure message. */
|
|
9
|
-
export const HeroStatement = forwardRef<HTMLElement, HeroProps>(
|
|
10
|
-
(
|
|
11
|
-
{ className, title, description, primaryAction, secondaryAction, media, meta, id, ...props },
|
|
12
|
-
ref,
|
|
13
|
-
) => {
|
|
14
|
-
// Statement paints its own background from --color-secondary, so the
|
|
15
|
-
// readable text color must be derived from THAT token, not the brand.
|
|
16
|
-
const fg = useTokenForeground("--color-secondary");
|
|
17
|
-
|
|
18
|
-
return (
|
|
19
|
-
<section
|
|
20
|
-
ref={ref}
|
|
21
|
-
id={id}
|
|
22
|
-
className={cn("relative w-full overflow-hidden bg-secondary", className)}
|
|
23
|
-
style={{ color: fg }}
|
|
24
|
-
{...props}
|
|
25
|
-
>
|
|
26
|
-
<div aria-hidden className="pointer-events-none absolute inset-0 grain opacity-[0.07] mix-blend-overlay" />
|
|
27
|
-
<div aria-hidden className="pointer-events-none absolute inset-x-0 top-0 h-px bg-white/10" />
|
|
28
|
-
<div className={cn(heroContainer, "pt-28 pb-24 text-center sm:pt-36 sm:pb-32")}>
|
|
29
|
-
<h1
|
|
30
|
-
className="max-w-5xl font-display text-[2.9rem] font-semibold leading-[1.02] tracking-[-0.04em] text-balance sm:text-7xl lg:text-8xl"
|
|
31
|
-
style={{ color: fg }}
|
|
32
|
-
>
|
|
33
|
-
{title}
|
|
34
|
-
</h1>
|
|
35
|
-
{description ? (
|
|
36
|
-
<p className="mx-auto mt-8 max-w-xl text-pretty leading-relaxed sm:text-lg" style={{ color: fg, opacity: 0.72 }}>
|
|
37
|
-
{description}
|
|
38
|
-
</p>
|
|
39
|
-
) : null}
|
|
40
|
-
{primaryAction || secondaryAction ? (
|
|
41
|
-
<div className="mt-11 justify-center">
|
|
42
|
-
<HeroActions primaryAction={primaryAction} secondaryAction={secondaryAction} />
|
|
43
|
-
</div>
|
|
44
|
-
) : null}
|
|
45
|
-
{meta ? (
|
|
46
|
-
<ul className="mt-16 flex flex-wrap items-center justify-center gap-x-10 gap-y-3">
|
|
47
|
-
{meta.map((m) => (
|
|
48
|
-
<li key={m.label} className="flex items-center gap-2 text-sm font-medium" style={{ color: fg, opacity: 0.72 }}>
|
|
49
|
-
{m.icon ? <span>{m.icon}</span> : null}
|
|
50
|
-
{m.label}
|
|
51
|
-
</li>
|
|
52
|
-
))}
|
|
53
|
-
</ul>
|
|
54
|
-
) : null}
|
|
55
|
-
</div>
|
|
56
|
-
</section>
|
|
57
|
-
);
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
HeroStatement.displayName = "HeroStatement";
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
-
import { LanguageSwitcher } from "./LanguageSwitcher";
|
|
4
|
-
|
|
5
|
-
const languages = [
|
|
6
|
-
{ code: "tr", label: "Türkçe" },
|
|
7
|
-
{ code: "en", label: "English" },
|
|
8
|
-
];
|
|
9
|
-
|
|
10
|
-
function trigger(container: HTMLElement): HTMLButtonElement {
|
|
11
|
-
// The dropdown trigger is the only button with aria-haspopup="listbox".
|
|
12
|
-
return container.querySelector('button[aria-haspopup="listbox"]')!;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
describe("LanguageSwitcher", () => {
|
|
16
|
-
it("renders a trigger showing the current language (dropdown by default)", () => {
|
|
17
|
-
const { container } = render(
|
|
18
|
-
<LanguageSwitcher languages={languages} defaultValue="tr" />,
|
|
19
|
-
);
|
|
20
|
-
expect(trigger(container)).toBeInTheDocument();
|
|
21
|
-
expect(trigger(container)).toHaveTextContent("Türkçe");
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("opens a listbox and selects a language via onChange", () => {
|
|
25
|
-
const onChange = vi.fn();
|
|
26
|
-
const { container } = render(
|
|
27
|
-
<LanguageSwitcher languages={languages} defaultValue="en" onChange={onChange} />,
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
fireEvent.click(trigger(container));
|
|
31
|
-
expect(screen.getByRole("listbox")).toBeInTheDocument();
|
|
32
|
-
|
|
33
|
-
fireEvent.click(screen.getByRole("option", { name: /Türkçe/ }));
|
|
34
|
-
expect(onChange).toHaveBeenCalledWith("tr");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("marks the active option as selected", () => {
|
|
38
|
-
const { container } = render(
|
|
39
|
-
<LanguageSwitcher languages={languages} defaultValue="en" />,
|
|
40
|
-
);
|
|
41
|
-
fireEvent.click(trigger(container));
|
|
42
|
-
|
|
43
|
-
const enOption = screen.getByRole("option", { name: /English/ });
|
|
44
|
-
expect(enOption).toHaveAttribute("aria-selected", "true");
|
|
45
|
-
const trOption = screen.getByRole("option", { name: /Türkçe/ });
|
|
46
|
-
expect(trOption).toHaveAttribute("aria-selected", "false");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("collapses the dropdown after selecting a language", () => {
|
|
50
|
-
const { container } = render(
|
|
51
|
-
<LanguageSwitcher languages={languages} defaultValue="en" />,
|
|
52
|
-
);
|
|
53
|
-
fireEvent.click(trigger(container));
|
|
54
|
-
expect(trigger(container)).toHaveAttribute("aria-expanded", "true");
|
|
55
|
-
|
|
56
|
-
fireEvent.click(screen.getByRole("option", { name: /English/ }));
|
|
57
|
-
expect(trigger(container)).toHaveAttribute("aria-expanded", "false");
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("renders nothing when there are no languages", () => {
|
|
61
|
-
const { container } = render(<LanguageSwitcher languages={[]} />);
|
|
62
|
-
expect(container).toBeEmptyDOMElement();
|
|
63
|
-
});
|
|
64
|
-
});
|