diff --git a/demo/card.py b/demo/card.py
new file mode 100644
index 00000000..cebceb58
--- /dev/null
+++ b/demo/card.py
@@ -0,0 +1,103 @@
+import mesop as me
+
+
+def load(e: me.LoadEvent):
+ me.set_theme_mode("system")
+
+
+@me.page(
+ on_load=load,
+ security_policy=me.SecurityPolicy(
+ allowed_iframe_parents=["https://google.github.io"],
+ ),
+ path="/card",
+)
+def app():
+ with me.box(
+ style=me.Style(
+ display="flex",
+ flex_direction="column",
+ gap=15,
+ margin=me.Margin.all(15),
+ max_width=500,
+ )
+ ):
+ with me.card(appearance="outlined"):
+ me.card_header(
+ title="Grapefruit",
+ subtitle="Kind of fruit",
+ image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ )
+ me.image(
+ style=me.Style(
+ width="100%",
+ ),
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ )
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
+
+ with me.card_actions(align="end"):
+ me.button(label="Add to cart")
+ me.button(label="Buy")
+
+ with me.card(appearance="raised"):
+ me.card_header(
+ title="Grapefruit",
+ subtitle="Kind of fruit",
+ image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ image_type="small",
+ )
+
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
+
+ with me.card_actions(align="start"):
+ me.button(label="Add to cart")
+ me.button(label="Buy")
+
+ with me.card(appearance="outlined"):
+ me.card_header(
+ title="Grapefruit",
+ subtitle="Kind of fruit",
+ image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ image_type="medium",
+ )
+
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
+
+ with me.card_actions(align="start"):
+ me.button(label="Add to cart")
+ me.button(label="Buy")
+
+ me.card_header(
+ title="Grapefruit",
+ image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ image_type="large",
+ )
+
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
+
+ with me.card_actions(align="end"):
+ me.button(label="Add to cart")
+ me.button(label="Buy")
+
+ me.card_header(
+ title="Grapefruit",
+ image_type="large",
+ )
+
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
diff --git a/demo/main.py b/demo/main.py
index da29bec7..ed841cb8 100644
--- a/demo/main.py
+++ b/demo/main.py
@@ -28,6 +28,7 @@
import box as box
import button as button
import button_toggle as button_toggle
+import card as card
import chat as chat
import chat_inputs as chat_inputs
import checkbox as checkbox
@@ -183,6 +184,7 @@ class Section:
name="Visual",
examples=[
Example(name="badge"),
+ Example(name="card"),
Example(name="divider"),
Example(name="icon"),
Example(name="progress_bar"),
diff --git a/docs/components/card.md b/docs/components/card.md
new file mode 100644
index 00000000..0c8b30c7
--- /dev/null
+++ b/docs/components/card.md
@@ -0,0 +1,18 @@
+## Overview
+
+Card is based on the [Angular Material card component](https://material.angular.io/components/card/overview).
+
+## Examples
+
+
+
+```python
+--8<-- "demo/card.py"
+```
+
+## API
+
+::: mesop.components.card.card.card
+::: mesop.components.card_content.card_content.card_header
+::: mesop.components.card_content.card_content.card_content
+::: mesop.components.card_actions.card_actions.card_actions
diff --git a/mesop/BUILD b/mesop/BUILD
index aab1cc97..e17ab033 100644
--- a/mesop/BUILD
+++ b/mesop/BUILD
@@ -23,6 +23,10 @@ py_library(
deps = [
":version",
# REF(//scripts/scaffold_component.py):insert_component_import
+ "//mesop/components/card_header:py",
+ "//mesop/components/card_actions:py",
+ "//mesop/components/card_content:py",
+ "//mesop/components/card:py",
"//mesop/components/button_toggle:py",
"//mesop/components/date_range_picker:py",
"//mesop/components/datepicker:py",
diff --git a/mesop/__init__.py b/mesop/__init__.py
index 38cd9705..53bb146f 100644
--- a/mesop/__init__.py
+++ b/mesop/__init__.py
@@ -69,6 +69,14 @@
from mesop.components.button_toggle.button_toggle import (
button_toggle as button_toggle,
)
+from mesop.components.card.card import card as card
+from mesop.components.card_actions.card_actions import (
+ card_actions as card_actions,
+)
+from mesop.components.card_content.card_content import (
+ card_content as card_content,
+)
+from mesop.components.card_header.card_header import card_header as card_header
from mesop.components.checkbox.checkbox import (
CheckboxChangeEvent as CheckboxChangeEvent,
)
diff --git a/mesop/components/card/BUILD b/mesop/components/card/BUILD
new file mode 100644
index 00000000..2695754d
--- /dev/null
+++ b/mesop/components/card/BUILD
@@ -0,0 +1,9 @@
+load("//mesop/components:defs.bzl", "mesop_component")
+
+package(
+ default_visibility = ["//build_defs:mesop_internal"],
+)
+
+mesop_component(
+ name = "card",
+)
diff --git a/mesop/components/card/__init__.py b/mesop/components/card/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mesop/components/card/card.ng.html b/mesop/components/card/card.ng.html
new file mode 100644
index 00000000..8319d171
--- /dev/null
+++ b/mesop/components/card/card.ng.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mesop/components/card/card.proto b/mesop/components/card/card.proto
new file mode 100644
index 00000000..f5261972
--- /dev/null
+++ b/mesop/components/card/card.proto
@@ -0,0 +1,7 @@
+syntax = "proto2";
+
+package mesop.components.card;
+
+message CardType {
+ optional string appearance = 1;
+}
diff --git a/mesop/components/card/card.py b/mesop/components/card/card.py
new file mode 100644
index 00000000..c6527e4e
--- /dev/null
+++ b/mesop/components/card/card.py
@@ -0,0 +1,33 @@
+from typing import Literal
+
+import mesop.components.card.card_pb2 as card_pb
+from mesop.component_helpers import (
+ Style,
+ insert_composite_component,
+ register_native_component,
+)
+
+
+@register_native_component
+def card(
+ *,
+ appearance: Literal["outlined", "raised"] = "outlined",
+ style: Style | None = None,
+ key: str | None = None,
+):
+ """
+ This function creates a card.
+
+ Args:
+ appearance: Card appearance style: outlined or raised.
+ style: Style for the component.
+ key: The component [key](../components/index.md#component-key).
+ """
+ return insert_composite_component(
+ key=key,
+ type_name="card",
+ style=style,
+ proto=card_pb.CardType(
+ appearance=appearance,
+ ),
+ )
diff --git a/mesop/components/card/card.ts b/mesop/components/card/card.ts
new file mode 100644
index 00000000..a0e2b361
--- /dev/null
+++ b/mesop/components/card/card.ts
@@ -0,0 +1,40 @@
+import {MatCardModule} from '@angular/material/card';
+import {Component, Input} from '@angular/core';
+import {
+ Key,
+ Type,
+ Style,
+} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
+import {CardType} from 'mesop/mesop/components/card/card_jspb_proto_pb/mesop/components/card/card_pb';
+import {formatStyle} from '../../web/src/utils/styles';
+
+@Component({
+ selector: 'mesop-card',
+ templateUrl: 'card.ng.html',
+ standalone: true,
+ imports: [MatCardModule],
+})
+export class CardComponent {
+ @Input({required: true}) type!: Type;
+ @Input() key!: Key;
+ @Input() style!: Style;
+ private _config!: CardType;
+
+ ngOnChanges() {
+ this._config = CardType.deserializeBinary(
+ this.type.getValue() as unknown as Uint8Array,
+ );
+ }
+
+ config(): CardType {
+ return this._config;
+ }
+
+ getStyle(): string {
+ return formatStyle(this.style);
+ }
+
+ getAppearance(): 'outlined' | 'raised' {
+ return this.config().getAppearance() as 'outlined' | 'raised';
+ }
+}
diff --git a/mesop/components/card/e2e/BUILD b/mesop/components/card/e2e/BUILD
new file mode 100644
index 00000000..f77d6e0f
--- /dev/null
+++ b/mesop/components/card/e2e/BUILD
@@ -0,0 +1,13 @@
+load("//build_defs:defaults.bzl", "py_library")
+
+package(
+ default_visibility = ["//build_defs:mesop_examples"],
+)
+
+py_library(
+ name = "e2e",
+ srcs = glob(["*.py"]),
+ deps = [
+ "//mesop",
+ ],
+)
diff --git a/mesop/components/card/e2e/__init__.py b/mesop/components/card/e2e/__init__.py
new file mode 100644
index 00000000..9cda027e
--- /dev/null
+++ b/mesop/components/card/e2e/__init__.py
@@ -0,0 +1 @@
+from . import card_app as card_app
diff --git a/mesop/components/card/e2e/card_app.py b/mesop/components/card/e2e/card_app.py
new file mode 100644
index 00000000..44ca94b3
--- /dev/null
+++ b/mesop/components/card/e2e/card_app.py
@@ -0,0 +1,25 @@
+import mesop as me
+
+
+@me.page(path="/components/card/e2e/card_app")
+def app():
+ with me.card(appearance="outlined"):
+ me.card_header(
+ title="Grapefruit",
+ subtitle="Kind of fruit",
+ image="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ )
+ me.image(
+ style=me.Style(
+ width="100%",
+ ),
+ src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+ )
+ with me.card_content():
+ me.text(
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed augue ultricies, laoreet nunc eget, ultricies augue. In ornare bibendum mauris vel sodales. Donec ut interdum felis. Nulla facilisi. Morbi a laoreet turpis, sed posuere arcu. Nam nisi neque, molestie vitae euismod eu, sollicitudin eu lectus. Pellentesque orci metus, finibus id faucibus et, ultrices quis dui. Duis in augue ac metus tristique lacinia."
+ )
+
+ with me.card_actions(align="end"):
+ me.button(label="Add to cart")
+ me.button(label="Buy")
diff --git a/mesop/components/card/e2e/card_test.ts b/mesop/components/card/e2e/card_test.ts
new file mode 100644
index 00000000..b05971cc
--- /dev/null
+++ b/mesop/components/card/e2e/card_test.ts
@@ -0,0 +1,30 @@
+import {test, expect} from '@playwright/test';
+
+test.describe('Card', () => {
+ test('renders card', async ({page}) => {
+ await page.goto('/components/card/e2e/card_app');
+ expect(
+ await page
+ .locator('mat-card')
+ .evaluate((el) => el.classList.contains('mat-mdc-card-outlined')),
+ ).toBeTruthy();
+ expect(await page.locator('mat-card-title').textContent()).toContain(
+ 'Grapefruit',
+ );
+ expect(await page.locator('mat-card-subtitle').textContent()).toContain(
+ 'Kind of fruit',
+ );
+ expect(await page.locator('mat-card-content').textContent()).toContain(
+ 'Lorem ipsum dolor sit amet',
+ );
+ expect(
+ await page
+ .locator('mat-card-actions')
+ .evaluate((el) =>
+ el.classList.contains('mat-mdc-card-actions-align-end'),
+ ),
+ ).toBeTruthy();
+ expect(await page.getByText('Add to cart')).toHaveCount(1);
+ expect(await page.getByText('Buy')).toHaveCount(1);
+ });
+});
diff --git a/mesop/components/card_actions/BUILD b/mesop/components/card_actions/BUILD
new file mode 100644
index 00000000..b224a23b
--- /dev/null
+++ b/mesop/components/card_actions/BUILD
@@ -0,0 +1,9 @@
+load("//mesop/components:defs.bzl", "mesop_component")
+
+package(
+ default_visibility = ["//build_defs:mesop_internal"],
+)
+
+mesop_component(
+ name = "card_actions",
+)
diff --git a/mesop/components/card_actions/__init__.py b/mesop/components/card_actions/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mesop/components/card_actions/card_actions.ng.html b/mesop/components/card_actions/card_actions.ng.html
new file mode 100644
index 00000000..2f2a5047
--- /dev/null
+++ b/mesop/components/card_actions/card_actions.ng.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mesop/components/card_actions/card_actions.proto b/mesop/components/card_actions/card_actions.proto
new file mode 100644
index 00000000..f1f15784
--- /dev/null
+++ b/mesop/components/card_actions/card_actions.proto
@@ -0,0 +1,7 @@
+syntax = "proto2";
+
+package mesop.components.card_actions;
+
+message CardActionsType {
+ optional string align = 1;
+}
diff --git a/mesop/components/card_actions/card_actions.py b/mesop/components/card_actions/card_actions.py
new file mode 100644
index 00000000..a826faa4
--- /dev/null
+++ b/mesop/components/card_actions/card_actions.py
@@ -0,0 +1,34 @@
+from typing import Literal
+
+import mesop.components.card_actions.card_actions_pb2 as card_actions_pb
+from mesop.component_helpers import (
+ insert_composite_component,
+ register_native_component,
+)
+
+
+@register_native_component
+def card_actions(
+ *,
+ align: Literal["start", "end"],
+ key: str | None = None,
+):
+ """
+ This function creates a card_actions.
+
+ This component is meant to be used with the `card` component. It is used for the
+ bottom area of a card that contains action buttons.
+
+ This component is a optional. It is mainly used as a convenience for consistent
+ formatting with the card component.
+
+ Args:
+ align: Align elements to the left (start) or right (end).
+ """
+ return insert_composite_component(
+ key=key,
+ type_name="card_actions",
+ proto=card_actions_pb.CardActionsType(
+ align=align,
+ ),
+ )
diff --git a/mesop/components/card_actions/card_actions.ts b/mesop/components/card_actions/card_actions.ts
new file mode 100644
index 00000000..9ce4f302
--- /dev/null
+++ b/mesop/components/card_actions/card_actions.ts
@@ -0,0 +1,33 @@
+import {MatCardModule} from '@angular/material/card';
+import {Component, Input} from '@angular/core';
+import {
+ Key,
+ Type,
+} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
+import {CardActionsType} from 'mesop/mesop/components/card_actions/card_actions_jspb_proto_pb/mesop/components/card_actions/card_actions_pb';
+
+@Component({
+ selector: 'mesop-card-actions',
+ templateUrl: 'card_actions.ng.html',
+ standalone: true,
+ imports: [MatCardModule],
+})
+export class CardActionsComponent {
+ @Input({required: true}) type!: Type;
+ @Input() key!: Key;
+ private _config!: CardActionsType;
+
+ ngOnChanges() {
+ this._config = CardActionsType.deserializeBinary(
+ this.type.getValue() as unknown as Uint8Array,
+ );
+ }
+
+ config(): CardActionsType {
+ return this._config;
+ }
+
+ getAlign(): 'start' | 'end' {
+ return this.config().getAlign() as 'start' | 'end';
+ }
+}
diff --git a/mesop/components/card_content/BUILD b/mesop/components/card_content/BUILD
new file mode 100644
index 00000000..7fe7841b
--- /dev/null
+++ b/mesop/components/card_content/BUILD
@@ -0,0 +1,9 @@
+load("//mesop/components:defs.bzl", "mesop_component")
+
+package(
+ default_visibility = ["//build_defs:mesop_internal"],
+)
+
+mesop_component(
+ name = "card_content",
+)
diff --git a/mesop/components/card_content/__init__.py b/mesop/components/card_content/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mesop/components/card_content/card_content.ng.html b/mesop/components/card_content/card_content.ng.html
new file mode 100644
index 00000000..f60c845e
--- /dev/null
+++ b/mesop/components/card_content/card_content.ng.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mesop/components/card_content/card_content.proto b/mesop/components/card_content/card_content.proto
new file mode 100644
index 00000000..3758dbdb
--- /dev/null
+++ b/mesop/components/card_content/card_content.proto
@@ -0,0 +1,7 @@
+syntax = "proto2";
+
+package mesop.components.card_content;
+
+message CardContentType {
+
+}
diff --git a/mesop/components/card_content/card_content.py b/mesop/components/card_content/card_content.py
new file mode 100644
index 00000000..df33830f
--- /dev/null
+++ b/mesop/components/card_content/card_content.py
@@ -0,0 +1,26 @@
+import mesop.components.card_content.card_content_pb2 as card_content_pb
+from mesop.component_helpers import (
+ insert_composite_component,
+ register_native_component,
+)
+
+
+@register_native_component
+def card_content(
+ *,
+ key: str | None = None,
+):
+ """
+ This function creates a card_content.
+
+ This component is meant to be used with the `card` component. It is used for the
+ contents of a card that
+
+ This component is a optional. It is mainly used as a convenience for consistent
+ formatting with the card component.
+ """
+ return insert_composite_component(
+ key=key,
+ type_name="card_content",
+ proto=card_content_pb.CardContentType(),
+ )
diff --git a/mesop/components/card_content/card_content.ts b/mesop/components/card_content/card_content.ts
new file mode 100644
index 00000000..cc8eec02
--- /dev/null
+++ b/mesop/components/card_content/card_content.ts
@@ -0,0 +1,29 @@
+import {MatCardModule} from '@angular/material/card';
+import {Component, Input} from '@angular/core';
+import {
+ Key,
+ Type,
+} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
+import {CardContentType} from 'mesop/mesop/components/card_content/card_content_jspb_proto_pb/mesop/components/card_content/card_content_pb';
+
+@Component({
+ selector: 'mesop-card-content',
+ templateUrl: 'card_content.ng.html',
+ standalone: true,
+ imports: [MatCardModule],
+})
+export class CardContentComponent {
+ @Input({required: true}) type!: Type;
+ @Input() key!: Key;
+ private _config!: CardContentType;
+
+ ngOnChanges() {
+ this._config = CardContentType.deserializeBinary(
+ this.type.getValue() as unknown as Uint8Array,
+ );
+ }
+
+ config(): CardContentType {
+ return this._config;
+ }
+}
diff --git a/mesop/components/card_header/BUILD b/mesop/components/card_header/BUILD
new file mode 100644
index 00000000..53e803c5
--- /dev/null
+++ b/mesop/components/card_header/BUILD
@@ -0,0 +1,9 @@
+load("//mesop/components:defs.bzl", "mesop_component")
+
+package(
+ default_visibility = ["//build_defs:mesop_internal"],
+)
+
+mesop_component(
+ name = "card_header",
+)
diff --git a/mesop/components/card_header/__init__.py b/mesop/components/card_header/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/mesop/components/card_header/card_header.ng.html b/mesop/components/card_header/card_header.ng.html
new file mode 100644
index 00000000..d71387ab
--- /dev/null
+++ b/mesop/components/card_header/card_header.ng.html
@@ -0,0 +1,28 @@
+@if (config().getImage() && config().getImageType() !== 'avatar') {
+
+
+ {{config().getTitle()}}
+ @if (config().getSubtitle()) {
+ {{config().getSubtitle()}}
+ } @if (config().getImageType() === 'small') {
+
+ } @if (config().getImageType() === 'medium') {
+
+ } @if (config().getImageType() === 'large') {
+
+ } @if (config().getImageType() === 'extra-large') {
+
+ }
+
+
+} @else {
+
+ @if (config().getImage()) {
+
+ }
+ {{config().getTitle()}}
+ @if (config().getSubtitle()) {
+ {{config().getSubtitle()}}
+ }
+
+}
diff --git a/mesop/components/card_header/card_header.proto b/mesop/components/card_header/card_header.proto
new file mode 100644
index 00000000..8d78a55d
--- /dev/null
+++ b/mesop/components/card_header/card_header.proto
@@ -0,0 +1,10 @@
+syntax = "proto2";
+
+package mesop.components.card_header;
+
+message CardHeaderType {
+ optional string title = 1;
+ optional string subtitle = 2;
+ optional string image = 3;
+ optional string image_type = 4;
+}
diff --git a/mesop/components/card_header/card_header.py b/mesop/components/card_header/card_header.py
new file mode 100644
index 00000000..e43be131
--- /dev/null
+++ b/mesop/components/card_header/card_header.py
@@ -0,0 +1,44 @@
+from typing import Literal
+
+import mesop.components.card_header.card_header_pb2 as card_header_pb
+from mesop.component_helpers import insert_component, register_native_component
+
+
+@register_native_component
+def card_header(
+ *,
+ title: str,
+ subtitle: str = "",
+ image: str = "",
+ image_type: Literal[
+ "avatar", "small", "medium", "large", "extra-large"
+ ] = "avatar",
+ key: str | None = None,
+):
+ """
+ This function creates a card_header.
+
+ This component is meant to be used with the `card` component. It is used for the
+ header of a card.
+
+ This component is a optional. It is mainly used as a convenience for consistent
+ formatting with the card component.
+
+ Args:
+ title: Title
+ subtitle: Optional subtitle
+ image: Optional image
+ image_type: Display style for the image. Avatar will display as a circular image
+ to the left of the title/subtitle. Small/medium/large/extra-large will display
+ a right-aligned image of the specified size.
+ """
+ insert_component(
+ key=key,
+ type_name="card_header",
+ proto=card_header_pb.CardHeaderType(
+ title=title,
+ subtitle=subtitle,
+ image=image,
+ image_type=image_type,
+ ),
+ )
diff --git a/mesop/components/card_header/card_header.ts b/mesop/components/card_header/card_header.ts
new file mode 100644
index 00000000..3345cf02
--- /dev/null
+++ b/mesop/components/card_header/card_header.ts
@@ -0,0 +1,30 @@
+import {MatCardModule} from '@angular/material/card';
+import {CommonModule} from '@angular/common';
+import {Component, Input} from '@angular/core';
+import {
+ Key,
+ Type,
+} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb';
+import {CardHeaderType} from 'mesop/mesop/components/card_header/card_header_jspb_proto_pb/mesop/components/card_header/card_header_pb';
+
+@Component({
+ selector: 'mesop-card-header',
+ templateUrl: 'card_header.ng.html',
+ standalone: true,
+ imports: [MatCardModule, CommonModule],
+})
+export class CardHeaderComponent {
+ @Input({required: true}) type!: Type;
+ @Input() key!: Key;
+ private _config!: CardHeaderType;
+
+ ngOnChanges() {
+ this._config = CardHeaderType.deserializeBinary(
+ this.type.getValue() as unknown as Uint8Array,
+ );
+ }
+
+ config(): CardHeaderType {
+ return this._config;
+ }
+}
diff --git a/mesop/example_index.py b/mesop/example_index.py
index df6cdb29..53ae1675 100644
--- a/mesop/example_index.py
+++ b/mesop/example_index.py
@@ -34,4 +34,5 @@
import mesop.components.datepicker.e2e as datepicker_e2e
import mesop.components.date_range_picker.e2e as date_range_picker_e2e
import mesop.components.button_toggle.e2e as button_toggle_e2e
+import mesop.components.card.e2e as card_e2e
# REF(//scripts/scaffold_component.py):insert_component_e2e_import_export
diff --git a/mesop/examples/BUILD b/mesop/examples/BUILD
index 8b6c1e94..fcb3dec7 100644
--- a/mesop/examples/BUILD
+++ b/mesop/examples/BUILD
@@ -15,6 +15,7 @@ py_library(
deps = [
"//demo",
# REF(//scripts/scaffold_component.py):insert_component_e2e_import
+ "//mesop/components/card/e2e",
"//mesop/components/button_toggle/e2e",
"//mesop/components/date_range_picker/e2e",
"//mesop/components/datepicker/e2e",
diff --git a/mesop/web/src/component_renderer/BUILD b/mesop/web/src/component_renderer/BUILD
index 348ee67b..d03def78 100644
--- a/mesop/web/src/component_renderer/BUILD
+++ b/mesop/web/src/component_renderer/BUILD
@@ -17,6 +17,10 @@ ng_module(
]) + ["component_renderer.css"],
deps = [
# REF(//scripts/scaffold_component.py):insert_component_import
+ "//mesop/components/card_header:ng",
+ "//mesop/components/card_actions:ng",
+ "//mesop/components/card_content:ng",
+ "//mesop/components/card:ng",
"//mesop/components/button_toggle:ng",
"//mesop/components/date_range_picker:ng",
"//mesop/components/datepicker:ng",
diff --git a/mesop/web/src/component_renderer/type_to_component.ts b/mesop/web/src/component_renderer/type_to_component.ts
index 16044ef4..34673ba1 100644
--- a/mesop/web/src/component_renderer/type_to_component.ts
+++ b/mesop/web/src/component_renderer/type_to_component.ts
@@ -1,3 +1,7 @@
+import {CardHeaderComponent} from '../../../components/card_header/card_header';
+import {CardActionsComponent} from '../../../components/card_actions/card_actions';
+import {CardContentComponent} from '../../../components/card_content/card_content';
+import {CardComponent} from '../../../components/card/card';
import {ButtonToggleComponent} from '../../../components/button_toggle/button_toggle';
import {DateRangePickerComponent} from '../../../components/date_range_picker/date_range_picker';
import {DatepickerComponent} from '../../../components/datepicker/datepicker';
@@ -59,6 +63,10 @@ export class UserDefinedComponent implements BaseComponent {
}
export const typeToComponent = {
+ 'card_header': CardHeaderComponent,
+ 'card_actions': CardActionsComponent,
+ 'card_content': CardContentComponent,
+ 'card': CardComponent,
'button_toggle': ButtonToggleComponent,
'date_range_picker': DateRangePickerComponent,
'datepicker': DatepickerComponent,
diff --git a/mkdocs.yml b/mkdocs.yml
index f0a38b95..9091faeb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -74,6 +74,7 @@ nav:
- Uploader: components/uploader.md
- Visual:
- Badge: components/badge.md
+ - Card: components/card.md
- Divider: components/divider.md
- Icon: components/icon.md
- Progress bar: components/progress-bar.md