From 7a54883f16bf17f184fd3c47f2443bfed63748a2 Mon Sep 17 00:00:00 2001 From: Richard To Date: Sun, 3 Nov 2024 12:11:42 -0800 Subject: [PATCH] Add Angular Material Card component (#1082) * Add Angular Material Card component This component consists of the following components: - card - card_header - card_content - card_actions I did not include card_footer since I couldn't tell what benefit it added, but we can added later if needed. I did not include an card_image component since it was easy enough to use image with width 100%. Closes #1080 --- demo/card.py | 103 ++++++++++++++++++ demo/main.py | 2 + docs/components/card.md | 18 +++ mesop/BUILD | 4 + mesop/__init__.py | 8 ++ mesop/components/card/BUILD | 9 ++ mesop/components/card/__init__.py | 0 mesop/components/card/card.ng.html | 3 + mesop/components/card/card.proto | 7 ++ mesop/components/card/card.py | 33 ++++++ mesop/components/card/card.ts | 40 +++++++ mesop/components/card/e2e/BUILD | 13 +++ mesop/components/card/e2e/__init__.py | 1 + mesop/components/card/e2e/card_app.py | 25 +++++ mesop/components/card/e2e/card_test.ts | 30 +++++ mesop/components/card_actions/BUILD | 9 ++ mesop/components/card_actions/__init__.py | 0 .../card_actions/card_actions.ng.html | 3 + .../card_actions/card_actions.proto | 7 ++ mesop/components/card_actions/card_actions.py | 34 ++++++ mesop/components/card_actions/card_actions.ts | 33 ++++++ mesop/components/card_content/BUILD | 9 ++ mesop/components/card_content/__init__.py | 0 .../card_content/card_content.ng.html | 3 + .../card_content/card_content.proto | 7 ++ mesop/components/card_content/card_content.py | 26 +++++ mesop/components/card_content/card_content.ts | 29 +++++ mesop/components/card_header/BUILD | 9 ++ mesop/components/card_header/__init__.py | 0 .../card_header/card_header.ng.html | 28 +++++ .../components/card_header/card_header.proto | 10 ++ mesop/components/card_header/card_header.py | 44 ++++++++ mesop/components/card_header/card_header.ts | 30 +++++ mesop/example_index.py | 1 + mesop/examples/BUILD | 1 + mesop/web/src/component_renderer/BUILD | 4 + .../component_renderer/type_to_component.ts | 8 ++ mkdocs.yml | 1 + 38 files changed, 592 insertions(+) create mode 100644 demo/card.py create mode 100644 docs/components/card.md create mode 100644 mesop/components/card/BUILD create mode 100644 mesop/components/card/__init__.py create mode 100644 mesop/components/card/card.ng.html create mode 100644 mesop/components/card/card.proto create mode 100644 mesop/components/card/card.py create mode 100644 mesop/components/card/card.ts create mode 100644 mesop/components/card/e2e/BUILD create mode 100644 mesop/components/card/e2e/__init__.py create mode 100644 mesop/components/card/e2e/card_app.py create mode 100644 mesop/components/card/e2e/card_test.ts create mode 100644 mesop/components/card_actions/BUILD create mode 100644 mesop/components/card_actions/__init__.py create mode 100644 mesop/components/card_actions/card_actions.ng.html create mode 100644 mesop/components/card_actions/card_actions.proto create mode 100644 mesop/components/card_actions/card_actions.py create mode 100644 mesop/components/card_actions/card_actions.ts create mode 100644 mesop/components/card_content/BUILD create mode 100644 mesop/components/card_content/__init__.py create mode 100644 mesop/components/card_content/card_content.ng.html create mode 100644 mesop/components/card_content/card_content.proto create mode 100644 mesop/components/card_content/card_content.py create mode 100644 mesop/components/card_content/card_content.ts create mode 100644 mesop/components/card_header/BUILD create mode 100644 mesop/components/card_header/__init__.py create mode 100644 mesop/components/card_header/card_header.ng.html create mode 100644 mesop/components/card_header/card_header.proto create mode 100644 mesop/components/card_header/card_header.py create mode 100644 mesop/components/card_header/card_header.ts diff --git a/demo/card.py b/demo/card.py new file mode 100644 index 000000000..cebceb585 --- /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 da29bec77..ed841cb86 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 000000000..0c8b30c7f --- /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 aab1cc97c..e17ab033a 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 38cd9705f..53bb146f5 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 000000000..2695754d2 --- /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 000000000..e69de29bb diff --git a/mesop/components/card/card.ng.html b/mesop/components/card/card.ng.html new file mode 100644 index 000000000..8319d171f --- /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 000000000..f5261972d --- /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 000000000..c6527e4e1 --- /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 000000000..a0e2b361a --- /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 000000000..f77d6e0f5 --- /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 000000000..9cda027e4 --- /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 000000000..44ca94b36 --- /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 000000000..b05971cc8 --- /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 000000000..b224a23bf --- /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 000000000..e69de29bb 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 000000000..2f2a50472 --- /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 000000000..f1f157841 --- /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 000000000..a826faa4c --- /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 000000000..9ce4f3021 --- /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 000000000..7fe7841b0 --- /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 000000000..e69de29bb 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 000000000..f60c845e3 --- /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 000000000..3758dbdbc --- /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 000000000..df33830ff --- /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 000000000..cc8eec021 --- /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 000000000..53e803c56 --- /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 000000000..e69de29bb 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 000000000..d71387abb --- /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 000000000..8d78a55d6 --- /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 000000000..e43be1316 --- /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 000000000..3345cf02e --- /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 df6cdb299..53ae16759 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 8b6c1e942..fcb3dec7d 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 348ee67b4..d03def78f 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 16044ef47..34673ba12 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 f0a38b95b..9091faeb7 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