Card source

Inspect and copy the reviewed source included with this Free component.

Free source · 4 files

recipe.rs

// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Card recipe — static named slots for a composable card surface.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const ROOT: &str = tw_join!(
        "rounded-xl",
        "border",
        "bg-card",
        "text-card-foreground",
        "shadow-sm"
    );
    pub const HEADER: &str = tw_join!("flex", "flex-col", "gap-1.5", "p-6");
    pub const TITLE: &str = tw_join!("leading-none", "font-semibold", "tracking-tight");
    pub const DESCRIPTION: &str = tw_join!("text-sm", "text-muted-foreground");
    pub const CONTENT: &str = tw_join!("p-6", "pt-0");
    pub const FOOTER: &str = tw_join!("flex", "items-center", "p-6", "pt-0");
}

/// Optional inputs accepted by [`CardRecipe::resolve`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct CardRecipeProps;

/// Static named classes for every Card part.
#[derive(Debug, Clone, Copy)]
pub struct CardRecipe {
    pub root: &'static str,
    pub header: &'static str,
    pub title: &'static str,
    pub description: &'static str,
    pub content: &'static str,
    pub footer: &'static str,
}

impl CardRecipe {
    pub const fn resolve(&'static self, _props: CardRecipeProps) -> CardStyle {
        CardStyle { recipe: self }
    }
}

/// Fully resolved, streamable Card root style.
#[must_use]
#[derive(Debug, Clone, Copy)]
pub struct CardStyle {
    recipe: &'static CardRecipe,
}

impl AttrValue for CardStyle {
    #[inline(always)]
    fn should_render(&self) -> bool {
        true
    }

    #[inline(always)]
    fn render_attr_value<B: WriteBuf, L: DataLoader>(
        &self,
        cx: &mut WebContext<L, B>,
    ) -> Result<(), WriteError> {
        cx.out.extend_static(self.recipe.root.as_bytes())
    }
}

pub const CARD_RECIPE: CardRecipe = CardRecipe {
    root: base::ROOT,
    header: base::HEADER,
    title: base::TITLE,
    description: base::DESCRIPTION,
    content: base::CONTENT,
    footer: base::FOOTER,
};

pub const fn card_recipe() -> &'static CardRecipe {
    &CARD_RECIPE
}

impl crate::variant_spec::ComponentSpec for CardRecipe {
    const NAME: &'static str = "Card";

    fn base_classes(&self) -> &'static str {
        self.root
    }

    fn variant_metadata(&self) -> Vec<crate::variant_spec::VariantMetadata> {
        Vec::new()
    }

    fn anatomy(&self) -> Option<crate::Anatomy> {
        Some(super::CARD_ANATOMY)
    }
}

/// Compatibility accessors for the former standalone helpers.
pub const fn card_classes() -> &'static str {
    CARD_RECIPE.root
}

pub const fn card_header_classes() -> &'static str {
    CARD_RECIPE.header
}

pub const fn card_title_classes() -> &'static str {
    CARD_RECIPE.title
}

pub const fn card_description_classes() -> &'static str {
    CARD_RECIPE.description
}

pub const fn card_content_classes() -> &'static str {
    CARD_RECIPE.content
}

pub const fn card_footer_classes() -> &'static str {
    CARD_RECIPE.footer
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn recipe_exposes_every_named_slot() {
        let recipe = card_recipe();
        assert!(recipe.root.contains("bg-card"));
        assert!(recipe.header.contains("flex-col"));
        assert!(recipe.title.contains("font-semibold"));
        assert!(recipe.description.contains("text-muted-foreground"));
        assert!(recipe.content.contains("pt-0"));
        assert!(recipe.footer.contains("items-center"));
    }

    #[test]
    fn recipe_is_const_and_copy() {
        const STYLE: CardStyle = CARD_RECIPE.resolve(CardRecipeProps);
        let copied = STYLE;
        assert!(copied.recipe.root.contains("rounded-xl"));
    }

    #[test]
    fn recipe_metadata_exposes_the_complete_anatomy() {
        let metadata = crate::variant_spec::component_metadata(card_recipe());
        let anatomy = metadata.anatomy.as_ref().unwrap();
        assert_eq!(metadata.name, "Card");
        assert!(metadata.variants.is_empty());
        assert_eq!(anatomy.scope, "card");
        assert_eq!(anatomy.parts, super::super::CARD_ANATOMY.parts());
        metadata.validate().unwrap();
    }
}

Copy this source with the Free Proa UI components it imports; those component files remain available from their own source pages.

Licensed under MIT OR Apache-2.0. Third-party notices remain attached to the installed payload.

Open Markdown
FreeCard

Composable content containers.

Install with the Proa CLI
proa ui add card

Installs the reviewed component source, dependencies, shared support files, and required legal notices.

CLI and registry setup
recipe.rs
// SPDX-FileCopyrightText: 2026 Proa Labs, Inc.
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Card recipe — static named slots for a composable card surface.

use crate::tw_join;
use proa_core::shared::attr_value::AttrValue;
use proa_core::{DataLoader, WebContext, WriteBuf, WriteError};

mod base {
    use super::*;

    pub const ROOT: &str = tw_join!(
        "rounded-xl",
        "border",
        "bg-card",
        "text-card-foreground",
        "shadow-sm"
    );
    pub const HEADER: &str = tw_join!("flex", "flex-col", "gap-1.5", "p-6");
    pub const TITLE: &str = tw_join!("leading-none", "font-semibold", "tracking-tight");
    pub const DESCRIPTION: &str = tw_join!("text-sm", "text-muted-foreground");

Search

Type at least 2 characters

Customize

Make it yours

Palette

Radius

Overview
Components
Settings

Build your own

Themed Proa UI surface

Preview
Components52
RecipesTyped
customer-portal