From a005b50cd5e4be241f12b8bad418d73ac4ff8b5c Mon Sep 17 00:00:00 2001 From: zhiyanzhaijie Date: Wed, 26 Nov 2025 01:15:26 +0800 Subject: [PATCH 1/3] Add Component Card --- README.md | 3 +- component.json | 3 +- playwright/card.spec.ts | 44 +++++++ preview/src/components/card/component.json | 21 ++++ preview/src/components/card/component.rs | 107 ++++++++++++++++++ preview/src/components/card/docs.md | 32 ++++++ preview/src/components/card/mod.rs | 3 + preview/src/components/card/style.css | 54 +++++++++ .../src/components/card/variants/main/mod.rs | 49 ++++++++ preview/src/components/mod.rs | 1 + 10 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 playwright/card.spec.ts create mode 100644 preview/src/components/card/component.json create mode 100644 preview/src/components/card/component.rs create mode 100644 preview/src/components/card/docs.md create mode 100644 preview/src/components/card/mod.rs create mode 100644 preview/src/components/card/style.css create mode 100644 preview/src/components/card/variants/main/mod.rs diff --git a/README.md b/README.md index aaa67e6f..82164e18 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,14 @@ Building styled and more featured component libraries on top of Dioxus Primitive We're still in the early days - Many components are still being created and stabilized. -28/29 +30/30 - [x] Accordion - [x] Alert Dialog - [x] Aspect Ratio - [x] Avatar - [x] Calendar +- [x] Card - [x] Checkbox - [x] Collapsible - [x] Context Menu diff --git a/component.json b/component.json index 628343eb..07e2e65a 100644 --- a/component.json +++ b/component.json @@ -33,6 +33,7 @@ "preview/src/components/context_menu", "preview/src/components/aspect_ratio", "preview/src/components/scroll_area", - "preview/src/components/date_picker" + "preview/src/components/date_picker", + "preview/src/components/card" ] } diff --git a/playwright/card.spec.ts b/playwright/card.spec.ts new file mode 100644 index 00000000..70769a46 --- /dev/null +++ b/playwright/card.spec.ts @@ -0,0 +1,44 @@ +import { test, expect } from "@playwright/test"; + +test("card component structure", async ({ page }) => { + await page.goto("http://127.0.0.1:8080/component/?name=card&", { + timeout: 20 * 60 * 1000, + }); + + // Find the card element + const card = page.locator(".card").nth(0); + + // Assert the card is visible with correct data-slot + await expect(card).toBeVisible(); + await expect(card).toHaveAttribute("data-slot", "card"); + + // Assert card-header structure + const cardHeader = card.locator(".card-header"); + await expect(cardHeader).toBeVisible(); + await expect(cardHeader).toHaveAttribute("data-slot", "card-header"); + + // Assert card-title structure + const cardTitle = card.locator(".card-title"); + await expect(cardTitle).toBeVisible(); + await expect(cardTitle).toHaveAttribute("data-slot", "card-title"); + + // Assert card-description structure + const cardDescription = card.locator(".card-description"); + await expect(cardDescription).toBeVisible(); + await expect(cardDescription).toHaveAttribute("data-slot", "card-description"); + + // Assert card-action structure + const cardAction = card.locator(".card-action"); + await expect(cardAction).toBeVisible(); + await expect(cardAction).toHaveAttribute("data-slot", "card-action"); + + // Assert card-content structure + const cardContent = card.locator(".card-content"); + await expect(cardContent).toBeVisible(); + await expect(cardContent).toHaveAttribute("data-slot", "card-content"); + + // Assert card-footer structure + const cardFooter = card.locator(".card-footer"); + await expect(cardFooter).toBeVisible(); + await expect(cardFooter).toHaveAttribute("data-slot", "card-footer"); +}); diff --git a/preview/src/components/card/component.json b/preview/src/components/card/component.json new file mode 100644 index 00000000..4aeb91d0 --- /dev/null +++ b/preview/src/components/card/component.json @@ -0,0 +1,21 @@ +{ + "name": "card", + "description": "A simple card component", + "authors": [ + "zhiyanzhaijie" + ], + "exclude": [ + "variants", + "docs.md", + "component.json" + ], + "cargoDependencies": [ + { + "name": "dioxus-primitives", + "git": "https://github.com/DioxusLabs/components" + } + ], + "globalAssets": [ + "../../../assets/dx-components-theme.css" + ] +} diff --git a/preview/src/components/card/component.rs b/preview/src/components/card/component.rs new file mode 100644 index 00000000..036749a7 --- /dev/null +++ b/preview/src/components/card/component.rs @@ -0,0 +1,107 @@ +use dioxus::prelude::*; + +#[component] +pub fn Card( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + div { + class: "card", + "data-slot": "card", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardHeader( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-header", + "data-slot": "card-header", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardTitle( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-title", + "data-slot": "card-title", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardDescription( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-description", + "data-slot": "card-description", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardAction( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-action", + "data-slot": "card-action", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardContent( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-content", + "data-slot": "card-content", + ..attributes, + {children} + } + } +} + +#[component] +pub fn CardFooter( + #[props(extends=GlobalAttributes)] attributes: Vec, + children: Element, +) -> Element { + rsx! { + div { + class: "card-footer", + "data-slot": "card-footer", + ..attributes, + {children} + } + } +} diff --git a/preview/src/components/card/docs.md b/preview/src/components/card/docs.md new file mode 100644 index 00000000..da98a235 --- /dev/null +++ b/preview/src/components/card/docs.md @@ -0,0 +1,32 @@ +The card component is a flexible container for grouping related content and actions. It provides a structured layout with optional header, content, and footer sections. + +## Component Structure + +```rust +// The Card component must wrap all card elements. +Card { + // CardHeader contains the title, description, and optional action. + CardHeader { + // CardTitle displays the main heading. + CardTitle { "Card Title" } + // CardDescription provides supporting text. + CardDescription { "Card description goes here." } + // CardAction positions action elements (e.g., buttons) in the header. + CardAction { + Button { "Action" } + } + } + // CardContent holds the main body content. + CardContent { + p { "Main content of the card." } + } + // CardFooter contains footer actions or information. + CardFooter { + Button { "Submit" } + } +} +``` + +## Layout Notes + +- When `CardAction` is present inside `CardHeader`, the header automatically switches to a two-column grid layout. diff --git a/preview/src/components/card/mod.rs b/preview/src/components/card/mod.rs new file mode 100644 index 00000000..a3527a11 --- /dev/null +++ b/preview/src/components/card/mod.rs @@ -0,0 +1,3 @@ +mod component; +pub use component::*; + diff --git a/preview/src/components/card/style.css b/preview/src/components/card/style.css new file mode 100644 index 00000000..19a06b99 --- /dev/null +++ b/preview/src/components/card/style.css @@ -0,0 +1,54 @@ +.card { + display: flex; + flex-direction: column; + + gap: 1.5rem; + padding: 1.5rem 0; + color: var(--secondary-color-4); + border: 1px solid var(--primary-color-6); + border-radius: 8px; + background-color: var(--primary-color-2); + box-shadow: 0 2px 10px rgb(0 0 0 / 10%); +} + +.card-header { + display: grid; + grid-template-rows: auto auto; + grid-auto-rows: min-content; + align-items: start; + gap: 0.5rem; + padding: 0 1.5rem; +} + +.card-header:has([data-slot="card-action"]) { + grid-template-columns: 1fr auto; +} + +.card-title { + line-height: 1; + font-weight: 600; + font-size: 1rem; +} + +.card-description { + color: var(--secondary-color-5); + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card-action { + grid-column-start: 2; + grid-row: 1 / span 2; + align-self: start; + justify-self: end; +} + +.card-content { + padding: 0 1.5rem; +} + +.card-footer { + display: flex; + align-items: center; + padding: 0 1.5rem; +} diff --git a/preview/src/components/card/variants/main/mod.rs b/preview/src/components/card/variants/main/mod.rs new file mode 100644 index 00000000..466971de --- /dev/null +++ b/preview/src/components/card/variants/main/mod.rs @@ -0,0 +1,49 @@ +use super::super::component::*; +use crate::components::button::{Button, ButtonVariant}; +use crate::components::input::Input; +use crate::components::label::Label; +use dioxus::prelude::*; + +#[component] +pub fn Demo() -> Element { + rsx! { + Card { style: "width: 100%; max-width: 24rem;", + CardHeader { + CardTitle { "Login to your account" } + CardDescription { "Enter your email below to login to your account" } + CardAction { + Button { variant: ButtonVariant::Ghost, "Sign Up" } + } + } + CardContent { + form { + div { style: "display: flex; flex-direction: column; gap: 1.5rem;", + div { style: "display: grid; gap: 0.5rem;", + Label { html_for: "email", "Email" } + Input { + id: "email", + r#type: "email", + placeholder: "m@example.com", + } + } + div { style: "display: grid; gap: 0.5rem;", + div { style: "display: flex; align-items: center;", + Label { html_for: "password", "Password" } + a { + href: "#", + style: "margin-left: auto; font-size: 0.875rem; color: var(--secondary-color-5); text-decoration: underline; text-underline-offset: 4px;", + "Forgot your password?" + } + } + Input { id: "password", r#type: "password" } + } + } + } + } + CardFooter { style: "flex-direction: column; gap: 0.5rem;", + Button { r#type: "submit", style: "width: 100%;", "Login" } + Button { variant: ButtonVariant::Outline, style: "width: 100%;", "Login with Google" } + } + } + } +} diff --git a/preview/src/components/mod.rs b/preview/src/components/mod.rs index 887b2bbe..65f9ed37 100644 --- a/preview/src/components/mod.rs +++ b/preview/src/components/mod.rs @@ -63,6 +63,7 @@ examples!( avatar, button, calendar[simple, internationalized, range, unavailable_dates], + card, checkbox, collapsible, context_menu, From 64304fb9f2444db60f5edcbac163f32669eedbf4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 28 Nov 2025 18:50:01 -0600 Subject: [PATCH 2/3] tweak dark mode card styling --- preview/src/components/card/style.css | 6 +++--- preview/src/components/input/style.css | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/preview/src/components/card/style.css b/preview/src/components/card/style.css index 19a06b99..8c1ca459 100644 --- a/preview/src/components/card/style.css +++ b/preview/src/components/card/style.css @@ -5,9 +5,9 @@ gap: 1.5rem; padding: 1.5rem 0; color: var(--secondary-color-4); - border: 1px solid var(--primary-color-6); - border-radius: 8px; - background-color: var(--primary-color-2); + border: 1px solid var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-5)); + border-radius: 1rem; + background-color: var(--light, var(--secondary-color-2)) var(--dark, var(--primary-color-3)); box-shadow: 0 2px 10px rgb(0 0 0 / 10%); } diff --git a/preview/src/components/input/style.css b/preview/src/components/input/style.css index 0725871b..9faf0c95 100644 --- a/preview/src/components/input/style.css +++ b/preview/src/components/input/style.css @@ -12,8 +12,7 @@ border-radius: 0.5rem; border-radius: calc(0.5rem); background: none; - background: var(--light, var(--primary-color)) - var(--dark, var(--primary-color-3)); + background-color: var(--light, var(--primary-color)) var(--dark, color-mix(in oklab, #FFFFFF26 30%, transparent)); box-shadow: inset 0 0 0 1px var(--light, var(--primary-color-6)) var(--dark, var(--primary-color-7)); color: var(--secondary-color-4); @@ -34,7 +33,7 @@ .input:hover:not(:disabled), .input:focus-visible { background: var(--light, var(--primary-color-4)) - var(--dark, var(--primary-color-5)); + var(--dark, color-mix(in oklab, #FFFFFF26 50%, transparent)); color: var(--secondary-color-1); outline: none; } From 8ed5d84cb778ebdfe97d747c10f554658f129f55 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 28 Nov 2025 18:50:43 -0600 Subject: [PATCH 3/3] remove card playwright test --- playwright/card.spec.ts | 44 ----------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 playwright/card.spec.ts diff --git a/playwright/card.spec.ts b/playwright/card.spec.ts deleted file mode 100644 index 70769a46..00000000 --- a/playwright/card.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from "@playwright/test"; - -test("card component structure", async ({ page }) => { - await page.goto("http://127.0.0.1:8080/component/?name=card&", { - timeout: 20 * 60 * 1000, - }); - - // Find the card element - const card = page.locator(".card").nth(0); - - // Assert the card is visible with correct data-slot - await expect(card).toBeVisible(); - await expect(card).toHaveAttribute("data-slot", "card"); - - // Assert card-header structure - const cardHeader = card.locator(".card-header"); - await expect(cardHeader).toBeVisible(); - await expect(cardHeader).toHaveAttribute("data-slot", "card-header"); - - // Assert card-title structure - const cardTitle = card.locator(".card-title"); - await expect(cardTitle).toBeVisible(); - await expect(cardTitle).toHaveAttribute("data-slot", "card-title"); - - // Assert card-description structure - const cardDescription = card.locator(".card-description"); - await expect(cardDescription).toBeVisible(); - await expect(cardDescription).toHaveAttribute("data-slot", "card-description"); - - // Assert card-action structure - const cardAction = card.locator(".card-action"); - await expect(cardAction).toBeVisible(); - await expect(cardAction).toHaveAttribute("data-slot", "card-action"); - - // Assert card-content structure - const cardContent = card.locator(".card-content"); - await expect(cardContent).toBeVisible(); - await expect(cardContent).toHaveAttribute("data-slot", "card-content"); - - // Assert card-footer structure - const cardFooter = card.locator(".card-footer"); - await expect(cardFooter).toBeVisible(); - await expect(cardFooter).toHaveAttribute("data-slot", "card-footer"); -});