"""HTMLCard - Visual card container with optional title."""
from __future__ import annotations
import html
from typing import Any
from animaid.containers.base import HTMLContainer, _to_css
from animaid.css_types import (
Color,
CSSValue,
RadiusSize,
ShadowSize,
Size,
Spacing,
)
[docs]
class HTMLCard(HTMLContainer):
"""A visual card container for grouping related content.
HTMLCard provides a bordered/shadowed container with optional title,
commonly used for dashboard panels, info cards, and grouped content.
Examples:
>>> from animaid import HTMLCard, HTMLString
>>> card = HTMLCard([
... HTMLString("User Profile").bold(),
... HTMLString("Name: Alice"),
... ])
>>> # Card with title
>>> card = HTMLCard(
... title="User Profile",
... children=[HTMLString("Name: Alice")],
... )
>>> # Styled card
>>> card = HTMLCard(content).shadow().rounded()
>>> card = HTMLCard(content).elevated() # Preset with larger shadow
"""
_title: str | None
def __init__(
self,
children: list[Any] | None = None,
*,
title: str | None = None,
**styles: str | CSSValue,
) -> None:
"""Create a new card container.
Args:
children: List of child elements.
title: Optional title text displayed at the top.
**styles: Initial CSS styles.
"""
super().__init__(children, **styles)
self._title = title
# Default card styles
self._styles.setdefault("background-color", "white")
self._styles.setdefault("border-radius", RadiusSize.DEFAULT.to_css())
self._styles.setdefault("padding", "16px")
[docs]
def render(self) -> str:
"""Render the card with optional title.
Returns:
HTML string with card structure.
"""
parts = []
# Render title if present
if self._title:
escaped_title = html.escape(self._title)
parts.append(
f'<div style="font-weight: bold; font-size: 1.1em; '
f'margin-bottom: 12px; padding-bottom: 8px; '
f'border-bottom: 1px solid #e5e7eb;">{escaped_title}</div>'
)
# Render children
parts.append(self._render_children())
content = "".join(parts)
attrs = self._build_attributes()
if attrs:
return f"<div {attrs}>{content}</div>"
return f"<div>{content}</div>"
# =========================================================================
# Title Methods
# =========================================================================
[docs]
def set_title(self, text: str | None) -> "HTMLCard":
"""Set or change the card title.
Args:
text: Title text, or None to remove title.
Returns:
Self for method chaining.
"""
self._title = text
self._notify()
return self
@property
def title(self) -> str | None:
"""Get the card title."""
return self._title
# =========================================================================
# Shadow Methods
# =========================================================================
[docs]
def shadow(self, size: ShadowSize | str = ShadowSize.DEFAULT) -> "HTMLCard":
"""Add a box shadow to the card.
Args:
size: ShadowSize enum or CSS shadow string.
Returns:
Self for method chaining.
Example:
>>> card.shadow() # Default shadow
>>> card.shadow(ShadowSize.LG) # Larger shadow
"""
if isinstance(size, ShadowSize):
self._styles["box-shadow"] = size.to_css()
else:
self._styles["box-shadow"] = size
self._notify()
return self
[docs]
def no_shadow(self) -> "HTMLCard":
"""Remove the box shadow.
Returns:
Self for method chaining.
"""
self._styles["box-shadow"] = ShadowSize.NONE.to_css()
self._notify()
return self
# =========================================================================
# Border Radius Methods
# =========================================================================
[docs]
def rounded(self, size: RadiusSize | str = RadiusSize.DEFAULT) -> "HTMLCard":
"""Set the border radius (rounded corners).
Args:
size: RadiusSize enum or CSS radius string.
Returns:
Self for method chaining.
Example:
>>> card.rounded() # Default rounding
>>> card.rounded(RadiusSize.LG) # More rounded
"""
if isinstance(size, RadiusSize):
self._styles["border-radius"] = size.to_css()
else:
self._styles["border-radius"] = size
self._notify()
return self
[docs]
def no_rounded(self) -> "HTMLCard":
"""Remove border radius (sharp corners).
Returns:
Self for method chaining.
"""
self._styles["border-radius"] = RadiusSize.NONE.to_css()
self._notify()
return self
# =========================================================================
# Border Methods
# =========================================================================
[docs]
def bordered(self, color: Color | str = "#e5e7eb") -> "HTMLCard":
"""Add a border to the card.
Args:
color: Border color.
Returns:
Self for method chaining.
"""
self._styles["border"] = f"1px solid {_to_css(color)}"
self._notify()
return self
[docs]
def no_border(self) -> "HTMLCard":
"""Remove the border.
Returns:
Self for method chaining.
"""
if "border" in self._styles:
del self._styles["border"]
self._notify()
return self
# =========================================================================
# Background Methods
# =========================================================================
[docs]
def background(self, color: Color | str) -> "HTMLCard":
"""Set the background color.
Args:
color: Background color.
Returns:
Self for method chaining.
"""
self._styles["background-color"] = _to_css(color)
self._notify()
return self
# =========================================================================
# Presets
# =========================================================================
[docs]
def default(self) -> "HTMLCard":
"""Apply default card styling (light border, subtle shadow).
Returns:
Self for method chaining.
"""
self._styles["border"] = "1px solid #e5e7eb"
self._styles["box-shadow"] = ShadowSize.SM.to_css()
self._styles["border-radius"] = RadiusSize.DEFAULT.to_css()
self._notify()
return self
[docs]
def elevated(self) -> "HTMLCard":
"""Apply elevated card styling (prominent shadow).
Returns:
Self for method chaining.
"""
self._styles["box-shadow"] = ShadowSize.LG.to_css()
self._styles["border-radius"] = RadiusSize.LG.to_css()
if "border" in self._styles:
del self._styles["border"]
self._notify()
return self
[docs]
def outlined(self) -> "HTMLCard":
"""Apply outlined card styling (border only, no shadow).
Returns:
Self for method chaining.
"""
self._styles["border"] = "1px solid #e5e7eb"
self._styles["box-shadow"] = ShadowSize.NONE.to_css()
self._notify()
return self
[docs]
def flat(self) -> "HTMLCard":
"""Apply flat card styling (no border or shadow).
Returns:
Self for method chaining.
"""
if "border" in self._styles:
del self._styles["border"]
self._styles["box-shadow"] = ShadowSize.NONE.to_css()
self._notify()
return self
[docs]
def filled(self, color: Color | str = "#f9fafb") -> "HTMLCard":
"""Apply filled card styling with background color.
Args:
color: Background color.
Returns:
Self for method chaining.
"""
self._styles["background-color"] = _to_css(color)
if "border" in self._styles:
del self._styles["border"]
self._styles["box-shadow"] = ShadowSize.NONE.to_css()
self._notify()
return self
# =========================================================================
# Override base methods for correct return type
# =========================================================================
[docs]
def styled(self, **styles: str | CSSValue) -> "HTMLCard":
"""Apply additional inline styles."""
super().styled(**styles)
return self
[docs]
def add_class(self, *class_names: str) -> "HTMLCard":
"""Add CSS classes."""
super().add_class(*class_names)
return self
[docs]
def gap(self, size: Size | str | int) -> "HTMLCard":
"""Set the gap between child elements."""
super().gap(size)
return self
[docs]
def padding(self, size: Spacing | Size | str | int) -> "HTMLCard":
"""Set internal padding."""
super().padding(size)
return self
[docs]
def margin(self, size: Spacing | Size | str | int) -> "HTMLCard":
"""Set external margin."""
super().margin(size)
return self
[docs]
def width(self, size: Size | str | int) -> "HTMLCard":
"""Set container width."""
super().width(size)
return self
[docs]
def height(self, size: Size | str | int) -> "HTMLCard":
"""Set container height."""
super().height(size)
return self
[docs]
def max_width(self, size: Size | str | int) -> "HTMLCard":
"""Set maximum container width."""
super().max_width(size)
return self
[docs]
def min_width(self, size: Size | str | int) -> "HTMLCard":
"""Set minimum container width."""
super().min_width(size)
return self