@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,113 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type ButtonHTMLAttributes, type CSSProperties, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
import { getContrastText } from "../utils/contrast";
|
|
4
|
-
import { Slot } from "./Slot";
|
|
5
|
-
|
|
6
|
-
export type ButtonVariant = "primary" | "dark" | "outline" | "ghost" | "link";
|
|
7
|
-
export type ButtonSize = "sm" | "md" | "lg";
|
|
8
|
-
|
|
9
|
-
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
10
|
-
variant?: ButtonVariant;
|
|
11
|
-
size?: ButtonSize;
|
|
12
|
-
iconLeft?: ReactNode;
|
|
13
|
-
iconRight?: ReactNode;
|
|
14
|
-
/** Paints the button with an arbitrary CSS color; text color is computed for contrast. */
|
|
15
|
-
customColor?: string;
|
|
16
|
-
fullWidth?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* When `true`, the button styling is applied to a single child element
|
|
19
|
-
* instead of a `<button>`. Use it to render a framework router `<Link>`
|
|
20
|
-
* (Next.js, Remix, React Router…). `iconLeft` / `iconRight` are ignored;
|
|
21
|
-
* pass icons inside the child instead.
|
|
22
|
-
*/
|
|
23
|
-
asChild?: boolean;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const variants: Record<ButtonVariant, string> = {
|
|
27
|
-
primary: "bg-primary text-on-primary hover:bg-primary-hover",
|
|
28
|
-
dark: "bg-secondary text-on-secondary hover:bg-secondary-hover",
|
|
29
|
-
outline:
|
|
30
|
-
"border border-border bg-transparent text-foreground hover:border-foreground/30 hover:bg-surface",
|
|
31
|
-
ghost: "text-foreground hover:bg-surface",
|
|
32
|
-
link: "px-0 h-auto text-primary hover:text-primary-hover",
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const sizes: Record<ButtonSize, string> = {
|
|
36
|
-
sm: "h-9 px-4 text-[13px] gap-1.5",
|
|
37
|
-
md: "h-11 px-5 text-sm gap-2",
|
|
38
|
-
lg: "h-12 px-7 text-[15px] gap-2",
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const base =
|
|
42
|
-
"group inline-flex select-none items-center justify-center rounded-lg font-medium tracking-tight transition-[background-color,border-color,color,box-shadow] duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 cursor-pointer";
|
|
43
|
-
|
|
44
|
-
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
45
|
-
(
|
|
46
|
-
{
|
|
47
|
-
className,
|
|
48
|
-
variant = "primary",
|
|
49
|
-
size = "md",
|
|
50
|
-
iconLeft,
|
|
51
|
-
iconRight,
|
|
52
|
-
customColor,
|
|
53
|
-
fullWidth,
|
|
54
|
-
asChild,
|
|
55
|
-
style,
|
|
56
|
-
type = "button",
|
|
57
|
-
children,
|
|
58
|
-
...props
|
|
59
|
-
},
|
|
60
|
-
ref,
|
|
61
|
-
) => {
|
|
62
|
-
const customStyle: CSSProperties | undefined = customColor
|
|
63
|
-
? {
|
|
64
|
-
...style,
|
|
65
|
-
backgroundColor: customColor,
|
|
66
|
-
color: getContrastText(customColor),
|
|
67
|
-
}
|
|
68
|
-
: style;
|
|
69
|
-
|
|
70
|
-
const isLink = variant === "link";
|
|
71
|
-
|
|
72
|
-
const classes = cn(
|
|
73
|
-
base,
|
|
74
|
-
sizes[size],
|
|
75
|
-
customColor ? "hover:brightness-95" : variants[variant],
|
|
76
|
-
isLink && "underline-offset-4 hover:underline",
|
|
77
|
-
fullWidth && "w-full",
|
|
78
|
-
className,
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
if (asChild) {
|
|
82
|
-
return (
|
|
83
|
-
<Slot ref={ref} style={customStyle} className={classes} {...props}>
|
|
84
|
-
{children}
|
|
85
|
-
</Slot>
|
|
86
|
-
);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<button
|
|
91
|
-
ref={ref}
|
|
92
|
-
type={type}
|
|
93
|
-
style={customStyle}
|
|
94
|
-
className={classes}
|
|
95
|
-
{...props}
|
|
96
|
-
>
|
|
97
|
-
{iconLeft ? <span className={cn("shrink-0", !isLink && "opacity-70")}>{iconLeft}</span> : null}
|
|
98
|
-
{children}
|
|
99
|
-
{iconRight ? (
|
|
100
|
-
<span
|
|
101
|
-
className={cn(
|
|
102
|
-
"shrink-0 transition-transform duration-200",
|
|
103
|
-
!isLink && "opacity-70 group-hover:translate-x-0.5",
|
|
104
|
-
)}
|
|
105
|
-
>
|
|
106
|
-
{iconRight}
|
|
107
|
-
</span>
|
|
108
|
-
) : null}
|
|
109
|
-
</button>
|
|
110
|
-
);
|
|
111
|
-
},
|
|
112
|
-
);
|
|
113
|
-
Button.displayName = "Button";
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { CTA } from "./CTA";
|
|
4
|
-
|
|
5
|
-
const base = { title: "Ready to get started?" };
|
|
6
|
-
|
|
7
|
-
describe("CTA", () => {
|
|
8
|
-
it("renders the title and description", () => {
|
|
9
|
-
render(<CTA {...base} description="Free for 14 days." />);
|
|
10
|
-
expect(screen.getByText("Ready to get started?")).toBeInTheDocument();
|
|
11
|
-
expect(screen.getByText("Free for 14 days.")).toBeInTheDocument();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it("renders primary and secondary actions", () => {
|
|
15
|
-
render(
|
|
16
|
-
<CTA
|
|
17
|
-
{...base}
|
|
18
|
-
action={<a href="#a">Action</a>}
|
|
19
|
-
secondaryAction={<a href="#b">Secondary</a>}
|
|
20
|
-
/>,
|
|
21
|
-
);
|
|
22
|
-
expect(screen.getByRole("link", { name: "Action" })).toBeInTheDocument();
|
|
23
|
-
expect(screen.getByRole("link", { name: "Secondary" })).toBeInTheDocument();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("renders the panel option (default) with a radial brand background", () => {
|
|
27
|
-
const { container } = render(<CTA {...base} option="panel" />);
|
|
28
|
-
const panel = container.querySelector("div[class*='rounded-']");
|
|
29
|
-
expect(panel).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("renders the surface option as a paper band", () => {
|
|
33
|
-
const { container } = render(<CTA {...base} option="surface" />);
|
|
34
|
-
expect(container.querySelector("section")).toHaveClass("w-full");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("renders the inverse option on a secondary surface", () => {
|
|
38
|
-
const { container } = render(<CTA {...base} option="inverse" />);
|
|
39
|
-
// The inverse option paints its band with the --color-secondary token.
|
|
40
|
-
const ink = container.querySelector("[style*='--color-secondary']");
|
|
41
|
-
expect(ink).toBeInTheDocument();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("applies centered alignment", () => {
|
|
45
|
-
render(<CTA {...base} option="surface" align="center" />);
|
|
46
|
-
expect(screen.getByText("Ready to get started?")).toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("forwards an id", () => {
|
|
50
|
-
const { container } = render(<CTA {...base} id="cta" />);
|
|
51
|
-
expect(container.querySelector("section#cta")).not.toBeNull();
|
|
52
|
-
});
|
|
53
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
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";
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
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";
|
|
@@ -1,60 +0,0 @@
|
|
|
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";
|
|
@@ -1,34 +0,0 @@
|
|
|
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";
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardHeader,
|
|
6
|
-
CardTitle,
|
|
7
|
-
CardDescription,
|
|
8
|
-
CardContent,
|
|
9
|
-
CardFooter,
|
|
10
|
-
} from "./Card";
|
|
11
|
-
|
|
12
|
-
describe("Card & subcomponents", () => {
|
|
13
|
-
it("renders an outlined card by default", () => {
|
|
14
|
-
render(<Card>Content</Card>);
|
|
15
|
-
expect(screen.getByText("Content")).toHaveClass("border-border");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("applies a custom variant", () => {
|
|
19
|
-
render(<Card variant="elevated">Elevated</Card>);
|
|
20
|
-
expect(screen.getByText("Elevated")).toHaveClass("shadow-soft");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("adds interactive styling when interactive", () => {
|
|
24
|
-
render(<Card interactive>Interactive</Card>);
|
|
25
|
-
expect(screen.getByText("Interactive")).toHaveClass("hover:-translate-y-0.5");
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("renders the full composite header/content/footer", () => {
|
|
29
|
-
render(
|
|
30
|
-
<Card>
|
|
31
|
-
<CardHeader>
|
|
32
|
-
<CardTitle>Title</CardTitle>
|
|
33
|
-
<CardDescription>Description</CardDescription>
|
|
34
|
-
</CardHeader>
|
|
35
|
-
<CardContent>Body</CardContent>
|
|
36
|
-
<CardFooter>Footer</CardFooter>
|
|
37
|
-
</Card>,
|
|
38
|
-
);
|
|
39
|
-
expect(screen.getByText("Title")).toBeInTheDocument();
|
|
40
|
-
expect(screen.getByText("Description")).toBeInTheDocument();
|
|
41
|
-
expect(screen.getByText("Body")).toBeInTheDocument();
|
|
42
|
-
expect(screen.getByText("Footer")).toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("renders an icon in the header when provided", () => {
|
|
46
|
-
render(<CardHeader icon={<span data-testid="hdr-icon" />}>Header</CardHeader>);
|
|
47
|
-
expect(screen.getByTestId("hdr-icon")).toBeInTheDocument();
|
|
48
|
-
});
|
|
49
|
-
});
|
package/src/components/Card.tsx
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
|
|
4
|
-
export type CardVariant = "outlined" | "elevated" | "inset";
|
|
5
|
-
|
|
6
|
-
export interface CardProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
-
variant?: CardVariant;
|
|
8
|
-
/** Softens the whole card with hover lift + border tint. */
|
|
9
|
-
interactive?: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const variants: Record<CardVariant, string> = {
|
|
13
|
-
outlined: "border border-border bg-surface",
|
|
14
|
-
elevated: "border border-border bg-surface shadow-soft",
|
|
15
|
-
inset: "bg-surface-strong",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const Card = forwardRef<HTMLDivElement, CardProps>(
|
|
19
|
-
({ className, variant = "outlined", interactive = false, ...props }, ref) => (
|
|
20
|
-
<div
|
|
21
|
-
ref={ref}
|
|
22
|
-
className={cn(
|
|
23
|
-
"rounded-xl transition-[border-color,box-shadow,transform] duration-200",
|
|
24
|
-
variants[variant],
|
|
25
|
-
interactive &&
|
|
26
|
-
"cursor-pointer hover:-translate-y-0.5 hover:border-foreground/20 hover:shadow-raised",
|
|
27
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
28
|
-
className,
|
|
29
|
-
)}
|
|
30
|
-
{...props}
|
|
31
|
-
/>
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
Card.displayName = "Card";
|
|
35
|
-
|
|
36
|
-
export interface CardHeaderProps extends HTMLAttributes<HTMLDivElement> {
|
|
37
|
-
icon?: ReactNode;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
|
|
41
|
-
({ className, icon, children, ...props }, ref) => (
|
|
42
|
-
<div ref={ref} className={cn("flex flex-col gap-2 p-6 pb-0", className)} {...props}>
|
|
43
|
-
{icon ? <div className="mb-1.5">{icon}</div> : null}
|
|
44
|
-
{children}
|
|
45
|
-
</div>
|
|
46
|
-
),
|
|
47
|
-
);
|
|
48
|
-
CardHeader.displayName = "CardHeader";
|
|
49
|
-
|
|
50
|
-
export const CardTitle = forwardRef<HTMLHeadingElement, HTMLAttributes<HTMLHeadingElement>>(
|
|
51
|
-
({ className, ...props }, ref) => (
|
|
52
|
-
<h3
|
|
53
|
-
ref={ref}
|
|
54
|
-
className={cn("font-display text-lg font-semibold tracking-tight text-foreground", className)}
|
|
55
|
-
{...props}
|
|
56
|
-
/>
|
|
57
|
-
),
|
|
58
|
-
);
|
|
59
|
-
CardTitle.displayName = "CardTitle";
|
|
60
|
-
|
|
61
|
-
export const CardDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
62
|
-
({ className, ...props }, ref) => (
|
|
63
|
-
<p ref={ref} className={cn("text-sm leading-6 text-muted-foreground", className)} {...props} />
|
|
64
|
-
),
|
|
65
|
-
);
|
|
66
|
-
CardDescription.displayName = "CardDescription";
|
|
67
|
-
|
|
68
|
-
export const CardContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
69
|
-
({ className, ...props }, ref) => (
|
|
70
|
-
<div ref={ref} className={cn("p-6 pt-5", className)} {...props} />
|
|
71
|
-
),
|
|
72
|
-
);
|
|
73
|
-
CardContent.displayName = "CardContent";
|
|
74
|
-
|
|
75
|
-
export const CardFooter = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
76
|
-
({ className, ...props }, ref) => (
|
|
77
|
-
<div ref={ref} className={cn("flex items-center gap-3 border-t border-border/70 p-6 pt-5", className)} {...props} />
|
|
78
|
-
),
|
|
79
|
-
);
|
|
80
|
-
CardFooter.displayName = "CardFooter";
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Container, Stack } from "./Container";
|
|
4
|
-
|
|
5
|
-
describe("Container", () => {
|
|
6
|
-
it("renders a div by default", () => {
|
|
7
|
-
render(<Container>Body</Container>);
|
|
8
|
-
const el = screen.getByText("Body");
|
|
9
|
-
expect(el.tagName).toBe("DIV");
|
|
10
|
-
expect(el).toHaveClass("mx-auto");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("applies a size class", () => {
|
|
14
|
-
render(<Container size="sm">Body</Container>);
|
|
15
|
-
expect(screen.getByText("Body")).toHaveClass("max-w-xl");
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("does not cap width for the full size", () => {
|
|
19
|
-
render(<Container size="full">Body</Container>);
|
|
20
|
-
expect(screen.getByText("Body")).not.toHaveClass("max-w-xl", "max-w-6xl");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("adds gutters by default and removes them with gutters=false", () => {
|
|
24
|
-
render(<Container>A</Container>);
|
|
25
|
-
expect(screen.getByText("A")).toHaveClass("px-5");
|
|
26
|
-
|
|
27
|
-
render(<Container gutters={false}>B</Container>);
|
|
28
|
-
expect(screen.getByText("B")).not.toHaveClass("px-5");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("renders as the requested element via `as`", () => {
|
|
32
|
-
render(<Container as="section">Body</Container>);
|
|
33
|
-
expect(screen.getByText("Body").tagName).toBe("SECTION");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe("Stack", () => {
|
|
38
|
-
it("renders a vertical flex by default", () => {
|
|
39
|
-
render(<Stack>A</Stack>);
|
|
40
|
-
expect(screen.getByText("A")).toHaveClass("flex", "flex-col");
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("renders horizontal when horizontal", () => {
|
|
44
|
-
render(<Stack horizontal>A</Stack>);
|
|
45
|
-
expect(screen.getByText("A")).toHaveClass("flex-row");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("applies a gap", () => {
|
|
49
|
-
render(<Stack gap={6}>A</Stack>);
|
|
50
|
-
expect(screen.getByText("A")).toHaveClass("gap-6");
|
|
51
|
-
});
|
|
52
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type ElementType, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
|
|
4
|
-
export type ContainerSize = "sm" | "md" | "lg" | "xl" | "full";
|
|
5
|
-
|
|
6
|
-
export interface ContainerProps extends HTMLAttributes<HTMLElement> {
|
|
7
|
-
/** Render as a specific element (section, header, …). */
|
|
8
|
-
as?: ElementType;
|
|
9
|
-
/**
|
|
10
|
-
* Maximum content width:
|
|
11
|
-
* `sm` → prose · `md` → reading · `lg` → plans/splits ·
|
|
12
|
-
* `xl` → default marketing page · `full` → edge-to-edge.
|
|
13
|
-
*/
|
|
14
|
-
size?: ContainerSize;
|
|
15
|
-
/** Responsive gutters (px-5 / sm:px-8). Disable for flush content. */
|
|
16
|
-
gutters?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const sizes: Record<Exclude<ContainerSize, "full">, string> = {
|
|
20
|
-
sm: "max-w-xl",
|
|
21
|
-
md: "max-w-3xl",
|
|
22
|
-
lg: "max-w-5xl",
|
|
23
|
-
xl: "max-w-6xl",
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Shared layout wrapper — the single source of the container + gutter rules
|
|
28
|
-
* used across Section, Footer, CTA, templates…
|
|
29
|
-
*/
|
|
30
|
-
export const Container = forwardRef<HTMLElement, ContainerProps>(
|
|
31
|
-
({ className, as, size = "xl", gutters = true, ...props }, ref) => {
|
|
32
|
-
const Tag = (as ?? "div") as ElementType;
|
|
33
|
-
return (
|
|
34
|
-
<Tag
|
|
35
|
-
ref={ref as never}
|
|
36
|
-
className={cn(
|
|
37
|
-
"mx-auto w-full",
|
|
38
|
-
size !== "full" && sizes[size],
|
|
39
|
-
gutters && "px-5 sm:px-8",
|
|
40
|
-
className,
|
|
41
|
-
)}
|
|
42
|
-
{...props}
|
|
43
|
-
/>
|
|
44
|
-
);
|
|
45
|
-
},
|
|
46
|
-
);
|
|
47
|
-
Container.displayName = "Container";
|
|
48
|
-
|
|
49
|
-
export type StackGap = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20;
|
|
50
|
-
|
|
51
|
-
const gaps: Record<StackGap, string> = {
|
|
52
|
-
0: "gap-0",
|
|
53
|
-
1: "gap-1",
|
|
54
|
-
2: "gap-2",
|
|
55
|
-
3: "gap-3",
|
|
56
|
-
4: "gap-4",
|
|
57
|
-
5: "gap-5",
|
|
58
|
-
6: "gap-6",
|
|
59
|
-
8: "gap-8",
|
|
60
|
-
10: "gap-10",
|
|
61
|
-
12: "gap-12",
|
|
62
|
-
16: "gap-16",
|
|
63
|
-
20: "gap-20",
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export interface StackProps extends HTMLAttributes<HTMLDivElement> {
|
|
67
|
-
gap?: StackGap;
|
|
68
|
-
horizontal?: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Vertical (or horizontal) rhythm helper using flex gaps. */
|
|
72
|
-
export const Stack = forwardRef<HTMLDivElement, StackProps>(
|
|
73
|
-
({ className, gap = 4, horizontal = false, ...props }, ref) => (
|
|
74
|
-
<div
|
|
75
|
-
ref={ref}
|
|
76
|
-
className={cn(
|
|
77
|
-
"flex",
|
|
78
|
-
horizontal ? "flex-row flex-wrap items-center" : "flex-col",
|
|
79
|
-
gaps[gap],
|
|
80
|
-
className,
|
|
81
|
-
)}
|
|
82
|
-
{...props}
|
|
83
|
-
/>
|
|
84
|
-
),
|
|
85
|
-
);
|
|
86
|
-
Stack.displayName = "Stack";
|