Card component

Compound API (Root/Header/Content/Footer/Title/Description) with data-scope/data-part attributes, CardPropsSchema, and CardMeta. 4 tests passing.
This commit is contained in:
Mats Bosson 2026-03-29 20:51:36 +07:00
parent 01286d8b07
commit 5c503ee9ef
9 changed files with 151 additions and 0 deletions

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardContentProps } from "./card.props";
/** The main content area of a Card. */
export function CardContent(props: CardContentProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<div data-scope="card" data-part="content" {...rest}>
{local.children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardDescriptionProps } from "./card.props";
/** A supporting description paragraph within a Card header. */
export function CardDescription(props: CardDescriptionProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<p data-scope="card" data-part="description" {...rest}>
{local.children}
</p>
);
}

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardFooterProps } from "./card.props";
/** The footer section of a Card, typically containing actions or metadata. */
export function CardFooter(props: CardFooterProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<div data-scope="card" data-part="footer" {...rest}>
{local.children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardHeaderProps } from "./card.props";
/** The header section of a Card, typically containing a title and description. */
export function CardHeader(props: CardHeaderProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<div data-scope="card" data-part="header" {...rest}>
{local.children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardRootProps } from "./card.props";
/** The root container element for a Card. */
export function CardRoot(props: CardRootProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<div data-scope="card" data-part="root" {...rest}>
{local.children}
</div>
);
}

View File

@ -0,0 +1,13 @@
import type { JSX } from "solid-js";
import { splitProps } from "solid-js";
import type { CardTitleProps } from "./card.props";
/** The heading element within a Card header. */
export function CardTitle(props: CardTitleProps): JSX.Element {
const [local, rest] = splitProps(props, ["children"]);
return (
<h3 data-scope="card" data-part="title" {...rest}>
{local.children}
</h3>
);
}

View File

@ -0,0 +1,18 @@
import { z } from "zod/v4";
import type { JSX } from "solid-js";
import type { ComponentMeta } from "../../meta";
export const CardPropsSchema = z.object({});
export interface CardRootProps extends JSX.HTMLAttributes<HTMLDivElement> { children?: JSX.Element; }
export interface CardHeaderProps extends JSX.HTMLAttributes<HTMLDivElement> { children?: JSX.Element; }
export interface CardContentProps extends JSX.HTMLAttributes<HTMLDivElement> { children?: JSX.Element; }
export interface CardFooterProps extends JSX.HTMLAttributes<HTMLDivElement> { children?: JSX.Element; }
export interface CardTitleProps extends JSX.HTMLAttributes<HTMLHeadingElement> { children?: JSX.Element; }
export interface CardDescriptionProps extends JSX.HTMLAttributes<HTMLParagraphElement> { children?: JSX.Element; }
export const CardMeta: ComponentMeta = {
name: "Card",
description: "Grouped content container with header, body, and footer sections",
parts: ["Root", "Header", "Content", "Footer", "Title", "Description"] as const,
requiredParts: ["Root", "Content"] as const,
} as const;

View File

@ -0,0 +1,15 @@
import { CardRoot } from "./card-root";
import { CardHeader } from "./card-header";
import { CardContent } from "./card-content";
import { CardFooter } from "./card-footer";
import { CardTitle } from "./card-title";
import { CardDescription } from "./card-description";
export const Card = Object.assign(CardRoot, {
Header: CardHeader,
Content: CardContent,
Footer: CardFooter,
Title: CardTitle,
Description: CardDescription,
});
export type { CardRootProps, CardHeaderProps, CardContentProps, CardFooterProps, CardTitleProps, CardDescriptionProps } from "./card.props";
export { CardPropsSchema, CardMeta } from "./card.props";

View File

@ -0,0 +1,40 @@
import { render, screen } from "@solidjs/testing-library";
import { describe, expect, it } from "vitest";
import { Card } from "../../../src/components/card/index";
import { CardPropsSchema, CardMeta } from "../../../src/components/card/card.props";
describe("Card", () => {
it("renders with compound API", () => {
render(() => (
<Card>
<Card.Header>
<Card.Title>Title</Card.Title>
<Card.Description>Description</Card.Description>
</Card.Header>
<Card.Content>Body content</Card.Content>
<Card.Footer>Footer</Card.Footer>
</Card>
));
expect(screen.getByText("Title")).toBeTruthy();
expect(screen.getByText("Description")).toBeTruthy();
expect(screen.getByText("Body content")).toBeTruthy();
expect(screen.getByText("Footer")).toBeTruthy();
});
it("renders as div element", () => {
render(() => <Card data-testid="card"><Card.Content>Content</Card.Content></Card>);
expect(screen.getByTestId("card").tagName).toBe("DIV");
});
it("schema validates empty props", () => {
expect(CardPropsSchema.safeParse({}).success).toBe(true);
});
it("meta has all required fields", () => {
expect(CardMeta.name).toBe("Card");
expect(CardMeta.parts).toContain("Root");
expect(CardMeta.parts).toContain("Header");
expect(CardMeta.parts).toContain("Content");
expect(CardMeta.parts).toContain("Footer");
});
});