@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
package/src/components/Prose.tsx
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
|
|
4
|
-
export type ProseSize = "sm" | "md" | "lg" | "xl" | "2xl";
|
|
5
|
-
|
|
6
|
-
export interface ProseProps extends HTMLAttributes<HTMLElement> {
|
|
7
|
-
/** Content scale. `lg` (prose-lg) is the comfortable blog default. */
|
|
8
|
-
size?: ProseSize;
|
|
9
|
-
/** Renders raw HTML (CMS / MDX output). Mutually exclusive with children. */
|
|
10
|
-
html?: string;
|
|
11
|
-
/** When true, removes the reading-width cap. */
|
|
12
|
-
wide?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const sizes: Record<ProseSize, string> = {
|
|
16
|
-
sm: "prose-sm",
|
|
17
|
-
md: "prose",
|
|
18
|
-
lg: "prose-lg",
|
|
19
|
-
xl: "prose-xl",
|
|
20
|
-
"2xl": "prose-2xl",
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Blog / article / documentation body text.
|
|
25
|
-
*
|
|
26
|
-
* Wraps Tailwind Typography (`@tailwindcss/typography`) and re-skins its
|
|
27
|
-
* colors, type scale and code blocks through the same design tokens as every
|
|
28
|
-
* other component, so dark palettes and font swaps work out of the box.
|
|
29
|
-
*/
|
|
30
|
-
export const Prose = forwardRef<HTMLElement, ProseProps>(
|
|
31
|
-
({ className, size = "lg", html, wide = false, children, ...props }, ref) => (
|
|
32
|
-
<article
|
|
33
|
-
ref={ref}
|
|
34
|
-
className={cn(
|
|
35
|
-
"prose prose-neutral",
|
|
36
|
-
sizes[size],
|
|
37
|
-
!wide && "max-w-3xl",
|
|
38
|
-
className,
|
|
39
|
-
)}
|
|
40
|
-
{...(html ? { dangerouslySetInnerHTML: { __html: html } } : null)}
|
|
41
|
-
{...props}
|
|
42
|
-
>
|
|
43
|
-
{html ? null : children}
|
|
44
|
-
</article>
|
|
45
|
-
),
|
|
46
|
-
);
|
|
47
|
-
Prose.displayName = "Prose";
|
|
48
|
-
|
|
49
|
-
/** Intro / stand-first paragraph styled with the `prose` lead class. */
|
|
50
|
-
export const ProseLead = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
51
|
-
({ className, ...props }, ref) => (
|
|
52
|
-
<p ref={ref} className={cn("lead", className)} {...props} />
|
|
53
|
-
),
|
|
54
|
-
);
|
|
55
|
-
ProseLead.displayName = "ProseLead";
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Section, SectionHeader } from "./Section";
|
|
4
|
-
|
|
5
|
-
describe("Section", () => {
|
|
6
|
-
it("renders a section element with padding for the size", () => {
|
|
7
|
-
render(<Section size="lg">Body</Section>);
|
|
8
|
-
const section = screen.getByText("Body").closest("section");
|
|
9
|
-
expect(section).toBeInTheDocument();
|
|
10
|
-
expect(section).toHaveClass("py-24");
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it("renders children", () => {
|
|
14
|
-
render(<Section>Body</Section>);
|
|
15
|
-
expect(screen.getByText("Body")).toBeInTheDocument();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it("forwards an id", () => {
|
|
19
|
-
render(<Section id="pricing">Pricing</Section>);
|
|
20
|
-
expect(screen.getByText("Pricing").closest("section")).toHaveAttribute("id", "pricing");
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it("renders a SectionHeader with eyebrow/title/description when provided", () => {
|
|
24
|
-
render(
|
|
25
|
-
<Section eyebrow="E" title="T" description="D">
|
|
26
|
-
Body
|
|
27
|
-
</Section>,
|
|
28
|
-
);
|
|
29
|
-
expect(screen.getByText("E")).toBeInTheDocument();
|
|
30
|
-
expect(screen.getByText("T")).toBeInTheDocument();
|
|
31
|
-
expect(screen.getByText("D")).toBeInTheDocument();
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("SectionHeader", () => {
|
|
36
|
-
it("renders eyebrow, title and description", () => {
|
|
37
|
-
render(<SectionHeader eyebrow="E" title="T" description="D" />);
|
|
38
|
-
expect(screen.getByText("E")).toBeInTheDocument();
|
|
39
|
-
expect(screen.getByText("T")).toBeInTheDocument();
|
|
40
|
-
expect(screen.getByText("D")).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("centers by default and aligns left when requested", () => {
|
|
44
|
-
const { rerender } = render(<SectionHeader title="T" />);
|
|
45
|
-
expect(screen.getByText("T").parentElement).toHaveClass("text-center");
|
|
46
|
-
|
|
47
|
-
rerender(<SectionHeader title="T" align="left" />);
|
|
48
|
-
// left is the base; center adds mx-auto text-center.
|
|
49
|
-
expect(screen.getByText("T").parentElement).not.toHaveClass("text-center");
|
|
50
|
-
});
|
|
51
|
-
});
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
-
import { cn } from "../utils/cn";
|
|
3
|
-
import { Container } from "./Container";
|
|
4
|
-
|
|
5
|
-
export type EyebrowStyle = "caps" | "soft" | "plain";
|
|
6
|
-
|
|
7
|
-
export interface SectionHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
8
|
-
eyebrow?: ReactNode;
|
|
9
|
-
eyebrowStyle?: EyebrowStyle;
|
|
10
|
-
title?: ReactNode;
|
|
11
|
-
description?: ReactNode;
|
|
12
|
-
align?: "left" | "center";
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const eyebrowBase = "inline-flex items-center gap-2 text-xs font-semibold tracking-[0.18em] uppercase";
|
|
16
|
-
|
|
17
|
-
const eyebrowStyles: Record<EyebrowStyle, string> = {
|
|
18
|
-
caps: cn(eyebrowBase, "text-foreground/50"),
|
|
19
|
-
soft: cn(
|
|
20
|
-
eyebrowBase,
|
|
21
|
-
"rounded-full px-3 py-1 text-primary tracking-widest bg-primary-soft uppercase",
|
|
22
|
-
),
|
|
23
|
-
plain: cn(eyebrowBase, "text-primary"),
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const SectionHeader = forwardRef<HTMLDivElement, SectionHeaderProps>(
|
|
27
|
-
(
|
|
28
|
-
{ className, eyebrow, eyebrowStyle = "caps", title, description, align = "center", children, ...props },
|
|
29
|
-
ref,
|
|
30
|
-
) => (
|
|
31
|
-
<div
|
|
32
|
-
ref={ref}
|
|
33
|
-
className={cn(
|
|
34
|
-
"mb-14 max-w-2xl",
|
|
35
|
-
align === "center" && "mx-auto text-center",
|
|
36
|
-
className,
|
|
37
|
-
)}
|
|
38
|
-
{...props}
|
|
39
|
-
>
|
|
40
|
-
{eyebrow ? <p className={eyebrowStyles[eyebrowStyle]}>{eyebrow}</p> : null}
|
|
41
|
-
{title ? (
|
|
42
|
-
<h2 className="mt-5 font-display text-balance text-[1.9rem] font-semibold leading-[1.08] tracking-tight text-foreground sm:text-4xl">
|
|
43
|
-
{title}
|
|
44
|
-
</h2>
|
|
45
|
-
) : null}
|
|
46
|
-
{description ? (
|
|
47
|
-
<p className="mt-4 text-pretty text-base leading-7 text-muted-foreground sm:text-lg">
|
|
48
|
-
{description}
|
|
49
|
-
</p>
|
|
50
|
-
) : null}
|
|
51
|
-
{children}
|
|
52
|
-
</div>
|
|
53
|
-
),
|
|
54
|
-
);
|
|
55
|
-
SectionHeader.displayName = "SectionHeader";
|
|
56
|
-
|
|
57
|
-
export interface SectionProps extends Omit<HTMLAttributes<HTMLElement>, "title"> {
|
|
58
|
-
size?: "sm" | "md" | "lg";
|
|
59
|
-
eyebrow?: ReactNode;
|
|
60
|
-
eyebrowStyle?: EyebrowStyle;
|
|
61
|
-
title?: ReactNode;
|
|
62
|
-
description?: ReactNode;
|
|
63
|
-
align?: "left" | "center";
|
|
64
|
-
containerClassName?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const paddings: Record<NonNullable<SectionProps["size"]>, string> = {
|
|
68
|
-
sm: "py-14",
|
|
69
|
-
md: "py-20 sm:py-24",
|
|
70
|
-
lg: "py-24 sm:py-32",
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export const Section = forwardRef<HTMLElement, SectionProps>(
|
|
74
|
-
(
|
|
75
|
-
{
|
|
76
|
-
className,
|
|
77
|
-
containerClassName,
|
|
78
|
-
size = "md",
|
|
79
|
-
eyebrow,
|
|
80
|
-
eyebrowStyle,
|
|
81
|
-
title,
|
|
82
|
-
description,
|
|
83
|
-
align = "center",
|
|
84
|
-
children,
|
|
85
|
-
id,
|
|
86
|
-
...props
|
|
87
|
-
},
|
|
88
|
-
ref,
|
|
89
|
-
) => (
|
|
90
|
-
<section ref={ref} id={id} className={cn("w-full", paddings[size], className)} {...props}>
|
|
91
|
-
<Container className={containerClassName}>
|
|
92
|
-
{eyebrow || title || description ? (
|
|
93
|
-
<SectionHeader
|
|
94
|
-
eyebrow={eyebrow}
|
|
95
|
-
eyebrowStyle={eyebrowStyle}
|
|
96
|
-
title={title}
|
|
97
|
-
description={description}
|
|
98
|
-
align={align}
|
|
99
|
-
/>
|
|
100
|
-
) : null}
|
|
101
|
-
{children}
|
|
102
|
-
</Container>
|
|
103
|
-
</section>
|
|
104
|
-
),
|
|
105
|
-
);
|
|
106
|
-
Section.displayName = "Section";
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Slot } from "./Slot";
|
|
4
|
-
|
|
5
|
-
describe("Slot", () => {
|
|
6
|
-
it("renders the single child with merged props", () => {
|
|
7
|
-
render(
|
|
8
|
-
<Slot className="btn" data-testid="slot">
|
|
9
|
-
<span>Hello</span>
|
|
10
|
-
</Slot>,
|
|
11
|
-
);
|
|
12
|
-
const el = screen.getByTestId("slot");
|
|
13
|
-
expect(el.tagName).toBe("SPAN");
|
|
14
|
-
expect(el).toHaveClass("btn");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("forwards an onClick to the child and calls both handlers", () => {
|
|
18
|
-
const slotClick = vi.fn();
|
|
19
|
-
const childClick = vi.fn();
|
|
20
|
-
render(
|
|
21
|
-
<Slot onClick={slotClick}>
|
|
22
|
-
<button onClick={childClick}>Go</button>
|
|
23
|
-
</Slot>,
|
|
24
|
-
);
|
|
25
|
-
const btn = screen.getByRole("button", { name: "Go" });
|
|
26
|
-
btn.click();
|
|
27
|
-
expect(childClick).toHaveBeenCalledTimes(1);
|
|
28
|
-
expect(slotClick).toHaveBeenCalledTimes(1);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("renders nothing when there is no single valid child", () => {
|
|
32
|
-
const { container } = render(<Slot>text only</Slot>);
|
|
33
|
-
// A plain string is not a valid React element, so Slot returns null.
|
|
34
|
-
expect(container).toBeEmptyDOMElement();
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("merges className with the child", () => {
|
|
38
|
-
render(
|
|
39
|
-
<Slot className="slot-class">
|
|
40
|
-
<div className="child-class">x</div>
|
|
41
|
-
</Slot>,
|
|
42
|
-
);
|
|
43
|
-
expect(screen.getByText("x")).toHaveClass("slot-class child-class");
|
|
44
|
-
});
|
|
45
|
-
});
|
package/src/components/Slot.tsx
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Children,
|
|
3
|
-
cloneElement,
|
|
4
|
-
forwardRef,
|
|
5
|
-
isValidElement,
|
|
6
|
-
type HTMLAttributes,
|
|
7
|
-
type ReactElement,
|
|
8
|
-
type ReactNode,
|
|
9
|
-
type Ref,
|
|
10
|
-
} from "react";
|
|
11
|
-
import { cn } from "../utils/cn";
|
|
12
|
-
|
|
13
|
-
export interface SlotProps extends Omit<HTMLAttributes<HTMLElement>, "ref"> {
|
|
14
|
-
children?: ReactNode;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type AnyProps = Record<string, any>;
|
|
18
|
-
|
|
19
|
-
function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps {
|
|
20
|
-
const overrideProps: AnyProps = { ...childProps };
|
|
21
|
-
|
|
22
|
-
for (const propName in childProps) {
|
|
23
|
-
const slotPropValue = slotProps[propName];
|
|
24
|
-
const childPropValue = childProps[propName];
|
|
25
|
-
const isHandler = /^on[A-Z]/.test(propName);
|
|
26
|
-
|
|
27
|
-
if (isHandler) {
|
|
28
|
-
if (slotPropValue && childPropValue) {
|
|
29
|
-
overrideProps[propName] = (...args: any[]) => {
|
|
30
|
-
childPropValue(...args);
|
|
31
|
-
slotPropValue(...args);
|
|
32
|
-
};
|
|
33
|
-
} else if (slotPropValue) {
|
|
34
|
-
overrideProps[propName] = slotPropValue;
|
|
35
|
-
}
|
|
36
|
-
} else if (propName === "style") {
|
|
37
|
-
overrideProps[propName] = { ...slotPropValue, ...childPropValue };
|
|
38
|
-
} else if (propName === "className") {
|
|
39
|
-
overrideProps[propName] = cn(slotPropValue, childPropValue);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { ...slotProps, ...overrideProps };
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function useComposedRefs(...refs: Array<Ref<unknown> | undefined>) {
|
|
47
|
-
return (node: unknown) => {
|
|
48
|
-
for (const ref of refs) {
|
|
49
|
-
if (typeof ref === "function") {
|
|
50
|
-
ref(node);
|
|
51
|
-
} else if (ref != null) {
|
|
52
|
-
(ref as { current: unknown }).current = node;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Merges its props onto a single child element instead of rendering its own
|
|
60
|
-
* DOM node. Used with the `asChild` prop so consumers can render a framework
|
|
61
|
-
* router `<Link>` (Next.js, Remix, React Router…) with a component's styling.
|
|
62
|
-
*/
|
|
63
|
-
export const Slot = forwardRef<HTMLElement, SlotProps>((props, forwardedRef) => {
|
|
64
|
-
const { children, ...slotProps } = props;
|
|
65
|
-
|
|
66
|
-
if (!isValidElement(children)) {
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const child = Children.only(children) as ReactElement & { ref?: Ref<unknown> };
|
|
71
|
-
const composedRef = useComposedRefs(forwardedRef, child.ref);
|
|
72
|
-
|
|
73
|
-
return cloneElement(child, {
|
|
74
|
-
...mergeProps(slotProps as AnyProps, child.props as AnyProps),
|
|
75
|
-
ref: composedRef,
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
Slot.displayName = "Slot";
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { render, screen } from "@testing-library/react";
|
|
3
|
-
import { Stats } from "./Stats";
|
|
4
|
-
|
|
5
|
-
const stats = [
|
|
6
|
-
{ value: "12.4K", label: "Active users", delta: 18.2 },
|
|
7
|
-
{ value: "98", suffix: "%", label: "Satisfaction", accent: true },
|
|
8
|
-
{ value: "1.8", suffix: "ms", label: "Response" },
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
describe("Stats", () => {
|
|
12
|
-
it("renders labels and values with the editorial option by default", () => {
|
|
13
|
-
render(<Stats stats={stats} />);
|
|
14
|
-
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
15
|
-
expect(screen.getByText("12.4K")).toBeInTheDocument();
|
|
16
|
-
expect(screen.getByText("Satisfaction")).toBeInTheDocument();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("renders a suffix inline with the value", () => {
|
|
20
|
-
render(<Stats stats={[stats[1]]} />);
|
|
21
|
-
expect(screen.getByText("%")).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("renders delta badges for statistics that have a delta", () => {
|
|
25
|
-
render(<Stats stats={[stats[0]]} />);
|
|
26
|
-
expect(screen.getByText("18.2%")).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("renders all option layouts", () => {
|
|
30
|
-
const { rerender } = render(<Stats stats={stats} option="editorial" />);
|
|
31
|
-
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
32
|
-
|
|
33
|
-
rerender(<Stats stats={stats} option="hairline" />);
|
|
34
|
-
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
35
|
-
|
|
36
|
-
rerender(<Stats stats={stats} option="cells" />);
|
|
37
|
-
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
38
|
-
|
|
39
|
-
rerender(<Stats stats={stats} option="ticker" />);
|
|
40
|
-
expect(screen.getByText("Active users")).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("uses a downward delta badge for negative changes", () => {
|
|
44
|
-
render(<Stats stats={[{ value: "1.8", label: "Response", delta: -24.6 }]} />);
|
|
45
|
-
expect(screen.getByText("24.6%")).toBeInTheDocument();
|
|
46
|
-
});
|
|
47
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import type { StatsProps } from "./types";
|
|
3
|
-
import { StatsEditorial } from "./variants/Editorial";
|
|
4
|
-
import { StatsHairline } from "./variants/Hairline";
|
|
5
|
-
import { StatsCells } from "./variants/Cells";
|
|
6
|
-
import { StatsTicker } from "./variants/Ticker";
|
|
7
|
-
|
|
8
|
-
export const Stats = forwardRef<HTMLDListElement, StatsProps>(
|
|
9
|
-
({ option = "editorial", ...props }, ref) => {
|
|
10
|
-
if (option === "ticker") return <StatsTicker ref={ref} {...props} />;
|
|
11
|
-
if (option === "cells") return <StatsCells ref={ref} {...props} />;
|
|
12
|
-
if (option === "hairline") return <StatsHairline ref={ref} {...props} />;
|
|
13
|
-
return <StatsEditorial ref={ref} {...props} />;
|
|
14
|
-
},
|
|
15
|
-
);
|
|
16
|
-
Stats.displayName = "Stats";
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { cn } from "../../utils/cn";
|
|
2
|
-
import { ArrowUp, ArrowDown } from "../../icons";
|
|
3
|
-
|
|
4
|
-
export const valueBase =
|
|
5
|
-
"font-display font-semibold tabular-nums tracking-[-0.02em] text-foreground";
|
|
6
|
-
|
|
7
|
-
export const suffixBase = "ml-1 font-sans text-sm font-normal tracking-normal text-muted-foreground";
|
|
8
|
-
|
|
9
|
-
export const gridCols: Record<2 | 3 | 4, string> = {
|
|
10
|
-
2: "sm:grid-cols-2",
|
|
11
|
-
3: "sm:grid-cols-2 lg:grid-cols-3",
|
|
12
|
-
4: "sm:grid-cols-2 lg:grid-cols-4",
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export function DeltaBadge({ delta }: { delta: number }) {
|
|
16
|
-
const isUp = delta >= 0;
|
|
17
|
-
const Icon = isUp ? ArrowUp : ArrowDown;
|
|
18
|
-
return (
|
|
19
|
-
<span
|
|
20
|
-
className={cn(
|
|
21
|
-
"inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-semibold tabular-nums",
|
|
22
|
-
isUp ? "bg-accent/10 text-accent" : "bg-danger-soft text-danger",
|
|
23
|
-
)}
|
|
24
|
-
>
|
|
25
|
-
<Icon className="h-3 w-3" />
|
|
26
|
-
{Math.abs(delta)}%
|
|
27
|
-
</span>
|
|
28
|
-
);
|
|
29
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
-
|
|
3
|
-
export type StatsOption = "editorial" | "hairline" | "cells" | "ticker";
|
|
4
|
-
|
|
5
|
-
export interface Stat {
|
|
6
|
-
value: string;
|
|
7
|
-
label: string;
|
|
8
|
-
/** Small unit rendered inline with the value, e.g. "ms" or "₺". */
|
|
9
|
-
suffix?: string;
|
|
10
|
-
/** Trend change in percent — renders an up/down badge. */
|
|
11
|
-
delta?: number;
|
|
12
|
-
/** Optional supporting copy. */
|
|
13
|
-
sub?: string;
|
|
14
|
-
icon?: ReactNode;
|
|
15
|
-
/** Highlights the value with the primary token. */
|
|
16
|
-
accent?: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface StatsProps extends HTMLAttributes<HTMLDListElement> {
|
|
20
|
-
stats: Stat[];
|
|
21
|
-
option?: StatsOption;
|
|
22
|
-
columns?: 2 | 3 | 4;
|
|
23
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { valueBase, suffixBase, gridCols } from "../parts";
|
|
4
|
-
import type { Stat } from "../types";
|
|
5
|
-
|
|
6
|
-
interface CellsProps extends HTMLAttributes<HTMLDListElement> {
|
|
7
|
-
stats: Stat[];
|
|
8
|
-
columns?: 2 | 3 | 4;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const StatsCells = forwardRef<HTMLDListElement, CellsProps>(
|
|
12
|
-
({ className, stats, columns = 4, ...props }, ref) => (
|
|
13
|
-
<dl ref={ref} className={cn("grid grid-cols-1 gap-4", gridCols[columns], className)} {...props}>
|
|
14
|
-
{stats.map((stat) => (
|
|
15
|
-
<div
|
|
16
|
-
key={stat.label}
|
|
17
|
-
className="flex min-h-[8.5rem] flex-col justify-between rounded-xl border border-border bg-surface p-6"
|
|
18
|
-
>
|
|
19
|
-
<dd className={cn(valueBase, "text-3xl", stat.accent ? "text-primary" : undefined)}>
|
|
20
|
-
{stat.value}
|
|
21
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
22
|
-
</dd>
|
|
23
|
-
<div className="mt-5 flex items-center justify-between gap-3">
|
|
24
|
-
<dt className="text-sm text-muted-foreground">{stat.label}</dt>
|
|
25
|
-
{stat.icon ? (
|
|
26
|
-
<span className="text-foreground/30">{stat.icon}</span>
|
|
27
|
-
) : null}
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
))}
|
|
31
|
-
</dl>
|
|
32
|
-
),
|
|
33
|
-
);
|
|
34
|
-
StatsCells.displayName = "StatsCells";
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { valueBase, suffixBase, gridCols, DeltaBadge } from "../parts";
|
|
4
|
-
import type { Stat } from "../types";
|
|
5
|
-
|
|
6
|
-
interface EditorialProps extends HTMLAttributes<HTMLDListElement> {
|
|
7
|
-
stats: Stat[];
|
|
8
|
-
columns?: 2 | 3 | 4;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** editorial — label above, oversized number, hairline rule under. */
|
|
12
|
-
export const StatsEditorial = forwardRef<HTMLDListElement, EditorialProps>(
|
|
13
|
-
({ className, stats, columns = 4, ...props }, ref) => (
|
|
14
|
-
<dl ref={ref} className={cn("grid grid-cols-1 gap-x-10 gap-y-12", gridCols[columns], className)} {...props}>
|
|
15
|
-
{stats.map((stat) => (
|
|
16
|
-
<div key={stat.label} className="border-t border-border pt-6">
|
|
17
|
-
<div className="flex items-center justify-between gap-3">
|
|
18
|
-
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted-foreground">
|
|
19
|
-
{stat.label}
|
|
20
|
-
</dt>
|
|
21
|
-
{typeof stat.delta === "number" ? <DeltaBadge delta={stat.delta} /> : null}
|
|
22
|
-
</div>
|
|
23
|
-
<dd
|
|
24
|
-
className={cn(
|
|
25
|
-
valueBase,
|
|
26
|
-
"mt-4 text-5xl sm:text-6xl",
|
|
27
|
-
stat.accent ? "text-primary" : undefined,
|
|
28
|
-
)}
|
|
29
|
-
>
|
|
30
|
-
{stat.value}
|
|
31
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
32
|
-
</dd>
|
|
33
|
-
{stat.sub ? (
|
|
34
|
-
<p className="mt-3 max-w-xs text-[13px] leading-5 text-muted-foreground">{stat.sub}</p>
|
|
35
|
-
) : null}
|
|
36
|
-
</div>
|
|
37
|
-
))}
|
|
38
|
-
</dl>
|
|
39
|
-
),
|
|
40
|
-
);
|
|
41
|
-
StatsEditorial.displayName = "StatsEditorial";
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { valueBase, suffixBase, gridCols } from "../parts";
|
|
4
|
-
import type { Stat } from "../types";
|
|
5
|
-
|
|
6
|
-
interface HairlineProps extends HTMLAttributes<HTMLDListElement> {
|
|
7
|
-
stats: Stat[];
|
|
8
|
-
columns?: 2 | 3 | 4;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const StatsHairline = forwardRef<HTMLDListElement, HairlineProps>(
|
|
12
|
-
({ className, stats, columns = 4, ...props }, ref) => (
|
|
13
|
-
<dl ref={ref} className={cn("grid grid-cols-1 gap-y-10", gridCols[columns], className)} {...props}>
|
|
14
|
-
{stats.map((stat, i) => (
|
|
15
|
-
<div
|
|
16
|
-
key={stat.label}
|
|
17
|
-
className={cn("text-center", columns > 1 && i > 0 && "sm:border-l sm:border-border")}
|
|
18
|
-
>
|
|
19
|
-
<dd className={cn(valueBase, "text-5xl", stat.accent ? "text-primary" : undefined)}>
|
|
20
|
-
{stat.value}
|
|
21
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
22
|
-
</dd>
|
|
23
|
-
<dt className="mt-3 text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground">
|
|
24
|
-
{stat.label}
|
|
25
|
-
</dt>
|
|
26
|
-
</div>
|
|
27
|
-
))}
|
|
28
|
-
</dl>
|
|
29
|
-
),
|
|
30
|
-
);
|
|
31
|
-
StatsHairline.displayName = "StatsHairline";
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
-
import { cn } from "../../../utils/cn";
|
|
3
|
-
import { valueBase, suffixBase, DeltaBadge } from "../parts";
|
|
4
|
-
import type { Stat } from "../types";
|
|
5
|
-
|
|
6
|
-
interface TickerProps extends HTMLAttributes<HTMLDListElement> {
|
|
7
|
-
stats: Stat[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export const StatsTicker = forwardRef<HTMLDListElement, TickerProps>(
|
|
11
|
-
({ className, stats, ...props }, ref) => (
|
|
12
|
-
<dl ref={ref} className={cn("flex w-full items-stretch overflow-x-auto rounded-2xl border border-border bg-surface", className)} {...props}>
|
|
13
|
-
{stats.map((stat, i) => (
|
|
14
|
-
<div
|
|
15
|
-
key={stat.label}
|
|
16
|
-
className={cn("flex min-w-[11rem] flex-1 flex-col gap-3 p-6", i > 0 && "border-l border-border")}
|
|
17
|
-
>
|
|
18
|
-
<div className="flex items-center justify-between gap-3">
|
|
19
|
-
<dt className="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
20
|
-
{stat.label}
|
|
21
|
-
</dt>
|
|
22
|
-
{typeof stat.delta === "number" ? <DeltaBadge delta={stat.delta} /> : null}
|
|
23
|
-
</div>
|
|
24
|
-
<dd className={cn(valueBase, "text-3xl", stat.accent ? "text-primary" : undefined)}>
|
|
25
|
-
{stat.value}
|
|
26
|
-
{stat.suffix ? <span className={suffixBase}>{stat.suffix}</span> : null}
|
|
27
|
-
</dd>
|
|
28
|
-
{stat.sub ? (
|
|
29
|
-
<p className="text-[13px] leading-5 text-muted-foreground">{stat.sub}</p>
|
|
30
|
-
) : null}
|
|
31
|
-
</div>
|
|
32
|
-
))}
|
|
33
|
-
</dl>
|
|
34
|
-
),
|
|
35
|
-
);
|
|
36
|
-
StatsTicker.displayName = "StatsTicker";
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { forwardRef } from "react";
|
|
2
|
-
import { cn } from "../../utils/cn";
|
|
3
|
-
import { getContrastText } from "../../utils/contrast";
|
|
4
|
-
import { Quote, Star } from "../../icons";
|
|
5
|
-
import { initials } from "./parts";
|
|
6
|
-
import type { TestimonialCardProps } from "./types";
|
|
7
|
-
|
|
8
|
-
export const TestimonialCard = forwardRef<HTMLElement, TestimonialCardProps>(
|
|
9
|
-
function TestimonialCard({ className, testimonial, ...props }, ref) {
|
|
10
|
-
const { quote, author, role, company, avatar, accent, rating } = testimonial;
|
|
11
|
-
const badgeStyle = accent
|
|
12
|
-
? { backgroundColor: accent, color: getContrastText(accent, "#111111", "#ffffff") }
|
|
13
|
-
: undefined;
|
|
14
|
-
|
|
15
|
-
return (
|
|
16
|
-
<figure
|
|
17
|
-
ref={ref}
|
|
18
|
-
className={cn(
|
|
19
|
-
"mb-4 flex flex-col break-inside-avoid rounded-2xl border border-border bg-surface p-7",
|
|
20
|
-
className,
|
|
21
|
-
)}
|
|
22
|
-
{...props}
|
|
23
|
-
>
|
|
24
|
-
<div className="mb-5 flex items-center justify-between">
|
|
25
|
-
<Quote className={cn("h-6 w-6", accent ? undefined : "text-primary/30")} style={accent ? { color: accent } : undefined} />
|
|
26
|
-
{rating ? (
|
|
27
|
-
<span className="flex items-center gap-0.5 text-amber-500" aria-label={`${rating} / 5`}>
|
|
28
|
-
{Array.from({ length: 5 }).map((_, i) => (
|
|
29
|
-
<Star key={i} className="h-3.5 w-3.5" />
|
|
30
|
-
))}
|
|
31
|
-
</span>
|
|
32
|
-
) : null}
|
|
33
|
-
</div>
|
|
34
|
-
<blockquote className="flex-1 text-[15px] leading-7 text-foreground/85">{quote}</blockquote>
|
|
35
|
-
<figcaption className="mt-6 flex items-center gap-3">
|
|
36
|
-
{avatar ? (
|
|
37
|
-
avatar
|
|
38
|
-
) : (
|
|
39
|
-
<span
|
|
40
|
-
style={badgeStyle}
|
|
41
|
-
className={cn(
|
|
42
|
-
"inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-xs font-bold",
|
|
43
|
-
!accent && "bg-primary-soft text-primary",
|
|
44
|
-
)}
|
|
45
|
-
>
|
|
46
|
-
{initials(author)}
|
|
47
|
-
</span>
|
|
48
|
-
)}
|
|
49
|
-
<div>
|
|
50
|
-
<div className="text-sm font-semibold text-foreground">{author}</div>
|
|
51
|
-
{(role || company) && (
|
|
52
|
-
<div className="text-xs text-muted-foreground">
|
|
53
|
-
{[role, company].filter(Boolean).join(" · ")}
|
|
54
|
-
</div>
|
|
55
|
-
)}
|
|
56
|
-
</div>
|
|
57
|
-
</figcaption>
|
|
58
|
-
</figure>
|
|
59
|
-
);
|
|
60
|
-
},
|
|
61
|
-
);
|