Skip to content

Commit f60e66e

Browse files
authored
Add CSS class support for box component (#995)
1 parent 429fa93 commit f60e66e

File tree

12 files changed

+274
-9
lines changed

12 files changed

+274
-9
lines changed

demo/bootstrap.py

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import mesop as me
2+
3+
4+
@me.page(
5+
security_policy=me.SecurityPolicy(
6+
allowed_iframe_parents=["https://google.github.io"]
7+
),
8+
stylesheets=[
9+
"https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css",
10+
],
11+
path="/bootstrap",
12+
)
13+
def page():
14+
with me.box(classes="container"):
15+
with me.box(
16+
classes="d-flex flex-wrap justify-content-between py-3 mb-4 border-bottom",
17+
):
18+
with me.box(
19+
classes="d-flex align-items-center mb-3 mb-md-0 me-md-auto link-body-emphasis text-decoration-none fs-4",
20+
):
21+
me.text("Mesop")
22+
23+
with me.box(classes="nav nav-pills"):
24+
with me.box(classes="nav-item"):
25+
with me.box(classes="nav-link active"):
26+
me.text("Features")
27+
with me.box(classes="nav-item"):
28+
with me.box(classes="nav-link"):
29+
me.text("About")
30+
31+
with me.box(classes="container px-4 py-5"):
32+
with me.box(classes="pb-2 border-bottom"):
33+
me.text("Columns", type="headline-5")
34+
35+
with me.box(classes="row g-4 py-5 row-cols-1 row-cols-lg-3"):
36+
with me.box(classes="feature col"):
37+
with me.box(classes="fs-2 text-body-emphasis"):
38+
me.text("Featured title")
39+
me.text(
40+
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
41+
)
42+
me.link(text="Call to action", url="/#")
43+
44+
with me.box(classes="feature col"):
45+
with me.box(classes="fs-2 text-body-emphasis"):
46+
me.text("Featured title")
47+
me.text(
48+
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
49+
)
50+
me.link(text="Call to action", url="/#")
51+
52+
with me.box(classes="feature col"):
53+
with me.box(classes="fs-2 text-body-emphasis"):
54+
me.text("Featured title")
55+
me.text(
56+
"Paragraph of text beneath the heading to explain the heading. We'll add onto it with another sentence and probably just keep going until we run out of words."
57+
)
58+
me.link(text="Call to action", url="/#")
59+
60+
with me.box(
61+
classes="d-flex flex-wrap justify-content-between align-items-center py-3 my-4 border-top"
62+
):
63+
with me.box(classes="col-md-4 mb-0 text-body-secondary"):
64+
me.text("Copyright 2024 Mesop")
65+
66+
with me.box(classes="nav col-md-4 justify-content-end"):
67+
with me.box(classes="nav-item"):
68+
with me.box(classes="nav-link px-2 text-body-secondary"):
69+
me.text("Home")
70+
with me.box(classes="nav-item"):
71+
with me.box(classes="nav-link px-2 text-body-secondary"):
72+
me.text("Features")
73+
with me.box(classes="nav-item"):
74+
with me.box(classes="nav-link px-2 text-body-secondary"):
75+
me.text("FAQs")
76+
with me.box(classes="nav-item"):
77+
with me.box(classes="nav-link px-2 text-body-secondary"):
78+
me.text("About")

demo/main.py

+7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import autocomplete as autocomplete
2525
import badge as badge
2626
import basic_animation as basic_animation
27+
import bootstrap as bootstrap
2728
import box as box
2829
import button as button
2930
import chat as chat
@@ -125,6 +126,12 @@ class Section:
125126
Example(name="feedback"),
126127
],
127128
),
129+
Section(
130+
name="Integrations",
131+
examples=[
132+
Example(name="bootstrap"),
133+
],
134+
),
128135
]
129136

130137
COMPONENTS_SECTIONS = [

mesop/components/box/box.proto

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ package mesop.components.box;
44

55
message BoxType {
66
optional string on_click_handler_id = 2;
7+
repeated string classes = 3;
78
}

mesop/components/box/box.py

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def box(
1515
*,
1616
style: Style | None = None,
1717
on_click: Callable[[ClickEvent], Any] | None = None,
18+
classes: list[str] | str = "",
1819
key: str | None = None,
1920
) -> Any:
2021
"""Creates a box component.
@@ -23,6 +24,7 @@ def box(
2324
style: Style to apply to component. Follows [HTML Element inline style API](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style).
2425
on_click: The callback function that is called when the box is clicked.
2526
It receives a ClickEvent as its only argument.
27+
classes: CSS classes.
2628
key: The component [key](../components/index.md#component-key).
2729
2830
Returns:
@@ -35,6 +37,7 @@ def box(
3537
on_click_handler_id=register_event_handler(on_click, event=ClickEvent)
3638
if on_click
3739
else "",
40+
classes=classes if isinstance(classes, list) else classes.split(" "),
3841
),
3942
style=style,
4043
)

mesop/editor/component_configs.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22
import inspect
33
from dataclasses import is_dataclass
44
from types import NoneType
5-
from typing import Any, Callable, ItemsView, Literal, Sequence
5+
from typing import (
6+
Any,
7+
Callable,
8+
ItemsView,
9+
Literal,
10+
Sequence,
11+
get_args,
12+
get_origin,
13+
)
614

715
import mesop.protos.ui_pb2 as pb
816
from mesop.exceptions import MesopInternalException
@@ -80,6 +88,14 @@ def get_fields(
8088
and args[1] is str
8189
and args[2] is NoneType
8290
)
91+
or (
92+
# special case, for list[str]|str (used for box classes), use str
93+
args
94+
and len(args) == 2
95+
and get_origin(args[0]) is list
96+
and get_args(args[0]) == (str,)
97+
and args[1] is str
98+
)
8399
):
84100
field_type = pb.FieldType(string_type=pb.StringType())
85101
elif getattr(param_type, "__origin__", None) is Literal:

mesop/examples/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from mesop.examples import starter_kit as starter_kit
4747
from mesop.examples import sxs as sxs
48+
from mesop.examples import tailwind as tailwind
4849
from mesop.examples import testing as testing
4950
from mesop.examples import viewport_size as viewport_size
5051
from mesop.examples import web_component as web_component

mesop/examples/tailwind.py

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"""
2+
Example Tailwind command:
3+
4+
```
5+
npx tailwindcss -i ./tailwind_input.css -o ./tailwind.css
6+
```
7+
8+
Example Tailwind config:
9+
10+
```
11+
/** @type {import('tailwindcss').Config} */
12+
module.exports = {
13+
content: ["main.py"],
14+
theme: {
15+
extend: {},
16+
},
17+
plugins: [],
18+
safelist: [],
19+
};
20+
```
21+
22+
Original HTML mark up:
23+
24+
```
25+
<html>
26+
<body class="min-h-screen flex flex-col">
27+
28+
<!-- Header -->
29+
<header class="bg-gray-800 text-white py-4">
30+
<div class="container mx-auto">
31+
<h1 class="text-2xl font-bold">Header</h1>
32+
</div>
33+
</header>
34+
35+
<!-- Main Content with Sidebar -->
36+
<div class="flex flex-1">
37+
<!-- Sidebar -->
38+
<aside class="w-64 bg-gray-200 p-4">
39+
<h2 class="text-lg font-semibold mb-4">Sidebar</h2>
40+
<ul>
41+
<li><a href="#" class="text-gray-700 block py-2">Link 1</a></li>
42+
<li><a href="#" class="text-gray-700 block py-2">Link 2</a></li>
43+
<li><a href="#" class="text-gray-700 block py-2">Link 3</a></li>
44+
</ul>
45+
</aside>
46+
47+
<!-- Main Content -->
48+
<main class="flex-1 p-6 bg-gray-100">
49+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
50+
<div class="bg-white p-6 rounded shadow">
51+
<h2 class="text-xl font-bold mb-2">Box 1</h2>
52+
<p>This is the content for box 1.</p>
53+
</div>
54+
<div class="bg-white p-6 rounded shadow">
55+
<h2 class="text-xl font-bold mb-2">Box 2</h2>
56+
<p>This is the content for box 2.</p>
57+
</div>
58+
<div class="bg-white p-6 rounded shadow">
59+
<h2 class="text-xl font-bold mb-2">Box 3</h2>
60+
<p>This is the content for box 3.</p>
61+
</div>
62+
</div>
63+
</main>
64+
</div>
65+
66+
<!-- Footer -->
67+
<footer class="bg-gray-800 text-white py-4">
68+
<div class="container mx-auto">
69+
<p>&copy; 2024 Your Company</p>
70+
</div>
71+
</footer>
72+
73+
</body>
74+
</html>
75+
```
76+
"""
77+
78+
import mesop as me
79+
80+
81+
@me.page(
82+
security_policy=me.SecurityPolicy(
83+
allowed_iframe_parents=["https://google.github.io"]
84+
),
85+
stylesheets=[
86+
# Specify your Tailwind CSS URL here.
87+
#
88+
# For local testing, you can just launch a basic Python HTTP server:
89+
# python -m http.server 8000
90+
"http://localhost:8000/assets/tailwind.css",
91+
],
92+
path="/tailwind",
93+
)
94+
def app():
95+
with me.box(classes="min-h-screen flex flex-col"):
96+
with me.box(classes="bg-gray-800 text-white py-4"):
97+
with me.box(classes="container mx-auto"):
98+
with me.box(classes="text-2xl font-bold"):
99+
me.text("Header")
100+
101+
with me.box(classes="flex flex-1"):
102+
with me.box(classes="w-64 bg-gray-200 p-4"):
103+
with me.box(classes="text-lg font-semibold mb-4"):
104+
me.text("Sidebar")
105+
with me.box(classes="text-gray-700 block py-2"):
106+
me.text("Link 1")
107+
with me.box(classes="text-gray-700 block py-2"):
108+
me.text("Link 2")
109+
with me.box(classes="text-gray-700 block py-2"):
110+
me.text("Link 3")
111+
112+
with me.box(classes="flex-1 p-6 bg-gray-100"):
113+
with me.box(classes="grid grid-cols-1 md:grid-cols-3 gap-4"):
114+
with me.box(classes="bg-white p-6 rounded shadow"):
115+
with me.box(classes="text-xl font-bold mb-2"):
116+
me.text("Box 1")
117+
me.text("This is the content for box 1.")
118+
119+
with me.box(classes="bg-white p-6 rounded shadow"):
120+
with me.box(classes="text-xl font-bold mb-2"):
121+
me.text("Box 2")
122+
me.text("This is the content for box 2")
123+
124+
with me.box(classes="bg-white p-6 rounded shadow"):
125+
with me.box(classes="text-xl font-bold mb-2"):
126+
me.text("Box 3")
127+
me.text("This is the content for box 3")
128+
129+
with me.box(classes="bg-gray-800 text-white py-4"):
130+
with me.box(classes="container mx-auto"):
131+
me.text("2024 Mesop")

mesop/web/src/app/styles.scss

+17
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,23 @@ body {
260260
}
261261
}
262262

263+
// The box component has div-like semantics, so make it `display: block` by default.
264+
// We use a high-level selector first since this will allow custom CSS selectors
265+
// to override this style more easily, specifically for flex and grid displays.
266+
component-renderer {
267+
display: block;
268+
}
269+
270+
// Custom elements like Angular component tags are treated as inline by default.
271+
//
272+
// Since component-renderer encompasses many different types of a components, we
273+
// need to add a more specific selector to make these inline by default.
274+
component-renderer:not([mesop-box]),
275+
// The first component-renderer object is a box, but it needs to be inline.
276+
mat-sidenav-content > component-renderer:first-child {
277+
display: inline;
278+
}
279+
263280
mesop-markdown {
264281
h1,
265282
h2,

mesop/web/src/component_renderer/component_renderer.ts

+14-3
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ export class ComponentRenderer {
176176
this._boxType = BoxType.deserializeBinary(
177177
this.component.getType()!.getValue() as unknown as Uint8Array,
178178
);
179+
// Used for determinine which component-renderer elements are not boxes.
180+
this.elementRef.nativeElement.setAttribute('mesop-box', 'true');
179181
}
180182

181183
this.computeStyles();
@@ -288,6 +290,10 @@ export class ComponentRenderer {
288290

289291
computeStyles() {
290292
this.elementRef.nativeElement.style = this.getStyle();
293+
const classes = this.getClasses();
294+
if (classes) {
295+
this.elementRef.nativeElement.classList = classes;
296+
}
291297
}
292298

293299
createComponentRef() {
@@ -395,9 +401,7 @@ Make sure the web component name is spelled the same between Python and JavaScri
395401
return '';
396402
}
397403

398-
// `display: block` because box should have "div"-like semantics.
399-
// Custom elements like Angular component tags are treated as inline by default.
400-
let style = 'display: block;';
404+
let style = '';
401405

402406
if (this.component.getStyle()) {
403407
style += formatStyle(this.component.getStyle()!);
@@ -407,6 +411,13 @@ Make sure the web component name is spelled the same between Python and JavaScri
407411
);
408412
}
409413

414+
getClasses(): string {
415+
if (this._boxType) {
416+
return this._boxType.getClassesList().join(' ');
417+
}
418+
return '';
419+
}
420+
410421
@HostListener('click', ['$event'])
411422
onClick(event: Event) {
412423
if (!this._boxType) {

mesop/web/src/dev_tools/editor_panel/editor_panel.scss

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
--default-bottom-panel-height: 400px;
88
}
99

10-
.container {
10+
.me-container {
1111
height: 100%;
1212
}
1313

mesop/web/src/editor/editor.ng.html

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
<mat-sidenav-container class="container">
2-
<mat-sidenav-content #sidenavContent class="content">
1+
<mat-sidenav-container class="me-container">
2+
<mat-sidenav-content #sidenavContent class="me-content">
33
<mesop-shell></mesop-shell>
44
@defer (when showEditorToolbar()) {
55
<mesop-editor-toolbar></mesop-editor-toolbar>

mesop/web/src/editor/editor.scss

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
.container {
1+
.me-container {
22
height: 100%;
33
}
44

5-
.content {
5+
.me-content {
66
overflow: hidden;
77
}
88

0 commit comments

Comments
 (0)