From 5c503ee9effc0c27d47f325e405016d5273d3806 Mon Sep 17 00:00:00 2001 From: Mats Bosson Date: Sun, 29 Mar 2026 20:51:36 +0700 Subject: [PATCH] Card component Compound API (Root/Header/Content/Footer/Title/Description) with data-scope/data-part attributes, CardPropsSchema, and CardMeta. 4 tests passing. --- .../core/src/components/card/card-content.tsx | 13 ++++++ .../src/components/card/card-description.tsx | 13 ++++++ .../core/src/components/card/card-footer.tsx | 13 ++++++ .../core/src/components/card/card-header.tsx | 13 ++++++ .../core/src/components/card/card-root.tsx | 13 ++++++ .../core/src/components/card/card-title.tsx | 13 ++++++ .../core/src/components/card/card.props.ts | 18 +++++++++ packages/core/src/components/card/index.ts | 15 +++++++ .../core/tests/components/card/card.test.tsx | 40 +++++++++++++++++++ 9 files changed, 151 insertions(+) create mode 100644 packages/core/src/components/card/card-content.tsx create mode 100644 packages/core/src/components/card/card-description.tsx create mode 100644 packages/core/src/components/card/card-footer.tsx create mode 100644 packages/core/src/components/card/card-header.tsx create mode 100644 packages/core/src/components/card/card-root.tsx create mode 100644 packages/core/src/components/card/card-title.tsx create mode 100644 packages/core/src/components/card/card.props.ts create mode 100644 packages/core/src/components/card/index.ts create mode 100644 packages/core/tests/components/card/card.test.tsx diff --git a/packages/core/src/components/card/card-content.tsx b/packages/core/src/components/card/card-content.tsx new file mode 100644 index 0000000..e63b6d9 --- /dev/null +++ b/packages/core/src/components/card/card-content.tsx @@ -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 ( +
+ {local.children} +
+ ); +} diff --git a/packages/core/src/components/card/card-description.tsx b/packages/core/src/components/card/card-description.tsx new file mode 100644 index 0000000..c11de95 --- /dev/null +++ b/packages/core/src/components/card/card-description.tsx @@ -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 ( +

+ {local.children} +

+ ); +} diff --git a/packages/core/src/components/card/card-footer.tsx b/packages/core/src/components/card/card-footer.tsx new file mode 100644 index 0000000..022b33d --- /dev/null +++ b/packages/core/src/components/card/card-footer.tsx @@ -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 ( +
+ {local.children} +
+ ); +} diff --git a/packages/core/src/components/card/card-header.tsx b/packages/core/src/components/card/card-header.tsx new file mode 100644 index 0000000..a3272fb --- /dev/null +++ b/packages/core/src/components/card/card-header.tsx @@ -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 ( +
+ {local.children} +
+ ); +} diff --git a/packages/core/src/components/card/card-root.tsx b/packages/core/src/components/card/card-root.tsx new file mode 100644 index 0000000..be5f8f3 --- /dev/null +++ b/packages/core/src/components/card/card-root.tsx @@ -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 ( +
+ {local.children} +
+ ); +} diff --git a/packages/core/src/components/card/card-title.tsx b/packages/core/src/components/card/card-title.tsx new file mode 100644 index 0000000..10253d6 --- /dev/null +++ b/packages/core/src/components/card/card-title.tsx @@ -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 ( +

+ {local.children} +

+ ); +} diff --git a/packages/core/src/components/card/card.props.ts b/packages/core/src/components/card/card.props.ts new file mode 100644 index 0000000..82d1359 --- /dev/null +++ b/packages/core/src/components/card/card.props.ts @@ -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 { children?: JSX.Element; } +export interface CardHeaderProps extends JSX.HTMLAttributes { children?: JSX.Element; } +export interface CardContentProps extends JSX.HTMLAttributes { children?: JSX.Element; } +export interface CardFooterProps extends JSX.HTMLAttributes { children?: JSX.Element; } +export interface CardTitleProps extends JSX.HTMLAttributes { children?: JSX.Element; } +export interface CardDescriptionProps extends JSX.HTMLAttributes { 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; diff --git a/packages/core/src/components/card/index.ts b/packages/core/src/components/card/index.ts new file mode 100644 index 0000000..3213c47 --- /dev/null +++ b/packages/core/src/components/card/index.ts @@ -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"; diff --git a/packages/core/tests/components/card/card.test.tsx b/packages/core/tests/components/card/card.test.tsx new file mode 100644 index 0000000..b4c2a71 --- /dev/null +++ b/packages/core/tests/components/card/card.test.tsx @@ -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(() => ( + + + Title + Description + + Body content + Footer + + )); + 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(() => Content); + 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"); + }); +});