diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..4a0f67b0c --- /dev/null +++ b/404.html @@ -0,0 +1,960 @@ + + + + + + + + + + + + + + + + + + + Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/commands/focus-component/index.html b/api/commands/focus-component/index.html new file mode 100644 index 000000000..0f57174eb --- /dev/null +++ b/api/commands/focus-component/index.html @@ -0,0 +1,1609 @@ + + + + + + + + + + + + + + + + + + + + + + + Focus component - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Focus component

+

If you want to focus on a component, you can use me.focus_component which focuses the component with the specified key if it is focusable.

+

Example

+
import mesop as me
+
+
+@me.page(path="/focus_component")
+def page():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.select(
+      options=[
+        me.SelectOption(label="Autocomplete", value="autocomplete"),
+        me.SelectOption(label="Checkbox", value="checkbox"),
+        me.SelectOption(label="Input", value="input"),
+        me.SelectOption(label="Link", value="link"),
+        me.SelectOption(label="Radio", value="radio"),
+        me.SelectOption(label="Select", value="select"),
+        me.SelectOption(label="Slider", value="slider"),
+        me.SelectOption(label="Slide Toggle", value="slide_toggle"),
+        me.SelectOption(label="Textarea", value="textarea"),
+        me.SelectOption(label="Uploader", value="uploader"),
+      ],
+      on_selection_change=on_selection_change,
+    )
+
+  me.divider()
+
+  with me.box(
+    style=me.Style(
+      display="grid",
+      gap=5,
+      grid_template_columns="1fr 1fr",
+      margin=me.Margin.all(15),
+    )
+  ):
+    with me.box():
+      me.autocomplete(
+        key="autocomplete",
+        label="Autocomplete",
+        options=[
+          me.AutocompleteOption(label="Test", value="Test"),
+          me.AutocompleteOption(label="Test2", value="Tes2t"),
+        ],
+      )
+
+    with me.box():
+      me.checkbox("Checkbox", key="checkbox")
+
+    with me.box():
+      me.input(key="input", label="Input")
+
+    with me.box():
+      me.link(key="link", text="Test", url="https://google.com")
+
+    with me.box():
+      me.radio(
+        key="radio",
+        options=[
+          me.RadioOption(label="Option 1", value="1"),
+          me.RadioOption(label="Option 2", value="2"),
+        ],
+      )
+
+    with me.box():
+      me.select(
+        key="select",
+        label="Select",
+        options=[
+          me.SelectOption(label="label 1", value="value1"),
+          me.SelectOption(label="label 2", value="value2"),
+          me.SelectOption(label="label 3", value="value3"),
+        ],
+      )
+
+    with me.box():
+      me.slider(key="slider")
+
+    with me.box():
+      me.slide_toggle(key="slide_toggle", label="Slide toggle")
+
+    with me.box():
+      me.textarea(key="textarea", label="Textarea")
+
+    with me.box():
+      me.uploader(
+        key="uploader",
+        label="Upload Image",
+        accepted_file_types=["image/jpeg", "image/png"],
+        type="flat",
+        color="primary",
+        style=me.Style(font_weight="bold"),
+      )
+
+
+def on_selection_change(e: me.SelectSelectionChangeEvent):
+  me.focus_component(key=e.value)
+
+

API

+ + +
+ + +

+ focus_component + +

+ + +
+ +

Focus the component specified by the key

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The unique identifier of the component to focus on. + This key should be globally unique to prevent unexpected behavior. + If multiple components share the same key, the first component + instance found in the component tree will be focused on.

+
+

+ + TYPE: + str + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/commands/navigate/index.html b/api/commands/navigate/index.html new file mode 100644 index 000000000..07afed1e9 --- /dev/null +++ b/api/commands/navigate/index.html @@ -0,0 +1,1547 @@ + + + + + + + + + + + + + + + + + + + + + + + Navigate - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Navigate

+

To navigate to another page, you can use me.navigate. This is particularly useful for navigating across a multi-page app.

+

Example

+
import mesop as me
+
+
+def navigate(event: me.ClickEvent):
+  me.navigate("/about")
+
+
+@me.page(path="/")
+def home():
+  me.text("This is the home page")
+  me.button("navigate to about page", on_click=navigate)
+
+
+@me.page(path="/about")
+def about():
+  me.text("This is the about page")
+
+

API

+ + +
+ + +

+ navigate + +

+ + +
+ +

Navigates to the given URL.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
url +
+

The URL to navigate to.

+
+

+ + TYPE: + str + +

+
query_params +
+

A dictionary of query parameters to include in the URL, or me.query_params. If not provided, all current query parameters will be removed.

+
+

+ + TYPE: + dict[str, str | Sequence[str]] | QueryParams | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/commands/scroll-into-view/index.html b/api/commands/scroll-into-view/index.html new file mode 100644 index 000000000..9f2943d36 --- /dev/null +++ b/api/commands/scroll-into-view/index.html @@ -0,0 +1,1568 @@ + + + + + + + + + + + + + + + + + + + + + + + Scroll into view - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Scroll into view

+

If you want to scroll a component into the viewport, you can use me.scroll_into_view which scrolls the component with the specified key into the viewport.

+

Example

+
import time
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  more_lines: int = 0
+
+
+@me.page(path="/scroll_into_view")
+def app():
+  me.button("Scroll to middle line", on_click=scroll_to_middle)
+  me.button("Scroll to bottom line", on_click=scroll_to_bottom)
+  me.button(
+    "Scroll to bottom line & generate lines",
+    on_click=scroll_to_bottom_and_generate_lines,
+  )
+  for _ in range(100):
+    me.text("Filler line")
+  me.text("middle_line", key="middle_line")
+  for _ in range(100):
+    me.text("Filler line")
+  me.text("bottom_line", key="bottom_line")
+  for _ in range(me.state(State).more_lines):
+    me.text("More lines")
+
+
+def scroll_to_middle(e: me.ClickEvent):
+  me.scroll_into_view(key="middle_line")
+
+
+def scroll_to_bottom(e: me.ClickEvent):
+  me.scroll_into_view(key="bottom_line")
+
+
+def scroll_to_bottom_and_generate_lines(e: me.ClickEvent):
+  state = me.state(State)
+  me.scroll_into_view(key="bottom_line")
+  yield
+  state.more_lines += 5
+  time.sleep(1)
+  yield
+  state.more_lines += 5
+  time.sleep(1)
+  yield
+  state.more_lines += 5
+  time.sleep(1)
+  yield
+  state.more_lines += 5
+  time.sleep(1)
+  yield
+
+

API

+ + +
+ + +

+ scroll_into_view + +

+ + +
+ +

Scrolls so the component specified by the key is in the viewport.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The unique identifier of the component to scroll to. + This key should be globally unique to prevent unexpected behavior. + If multiple components share the same key, the first component + instance found in the component tree will be scrolled to.

+
+

+ + TYPE: + str + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/commands/set-page-title/index.html b/api/commands/set-page-title/index.html new file mode 100644 index 000000000..fa195833e --- /dev/null +++ b/api/commands/set-page-title/index.html @@ -0,0 +1,1537 @@ + + + + + + + + + + + + + + + + + + + + + + + Set page title - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Set page title

+

If you want to set the page title, you can use me.set_page_title which will +set the page title displayed on the browser tab.

+

This change does not persist if you navigate to a new page. The title will be +reset to the title configured in me.page.

+

Example

+
import mesop as me
+
+
+def on_blur(e: me.InputBlurEvent):
+  me.set_page_title(e.value)
+
+
+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="/set_page_title",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.input(label="Page title", on_blur=on_blur)
+
+

API

+ + +
+ + +

+ set_page_title + +

+ + +
+ +

Sets the page title.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
title +
+

The new page title

+
+

+ + TYPE: + str + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/config/index.html b/api/config/index.html new file mode 100644 index 000000000..0c338f259 --- /dev/null +++ b/api/config/index.html @@ -0,0 +1,1985 @@ + + + + + + + + + + + + + + + + + + + + + + + Config - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Config

+

Overview

+

Mesop is configured at the application level using environment variables.

+

Configuration values

+

MESOP_STATIC_FOLDER

+
+

NOTE: By default, this feature is not enabled, but in an upcoming release, the +default will be static.

+
+

Allows access to static files from the Mesop server.

+

It is important to know that the specified folder path is relative to the current +working directory where the Mesop command is run. Absolute paths are not allowed.

+

Example:

+

In this case, the current working directory is /srv, which means Mesop will make +/srv/static the static folder.

+
cd /srv
+MESOP_STATIC_FOLDER=static mesop app/main.py
+
+

Here are some examples of valid paths. Let's assume the current working directory is +/srv/

+
    +
  • static becomes /srv/static
  • +
  • static/ becomes /srv/static
  • +
  • static/assets becomes /srv/static/assets
  • +
  • ./static becomes /srv/static
  • +
  • ./static/ becomes /srv/static
  • +
  • ./static/assets becomes /srv/static/assets
  • +
+

Invalid paths will raise MesopDeveloperException. Here are some examples:

+
    +
  • Absolute paths (e.g. /absolute/path)
  • +
  • .
  • +
  • ./
  • +
  • ..
  • +
  • ../
  • +
+

MESOP_STATIC_URL_PATH

+

This is the base URL path from which files for your specified static folder will be +made viewable.

+

The static URL path is only recognized if MESOP_STATIC_FOLDER is set.

+

For example, given MESOP_STATIC_FOLDER=static and MESOP_STATIC_URL_PATH=/assets, the +file static/js/script.js can be viewable from the URL path /assets/js/script.js.

+

Default: /static

+

MESOP_STATE_SESSION_BACKEND

+

Sets the backend to use for caching state data server-side. This makes it so state does +not have to be sent to the server on every request, reducing bandwidth, especially if +you have large state objects.

+

The backend options available at the moment are memory, file, sql, and firestore.

+

memory

+

Users should be careful when using the memory backend. Each Mesop process has their +own RAM, which means cache misses will be common if each server has multiple processes +and there is no session affinity. In addition, the amount of RAM must be carefully +specified per instance in accordance with the expected user traffic and state size.

+

The safest option for using the memory backend is to use a single process with a +good amount of RAM. Python is not the most memory efficient, especially when saving data +structures such as dicts.

+

The drawback of being limited to a single process is that requests will take longer to +process since only one request can be handled at a time. This is especially problematic +if your application contains long running API calls.

+

If session affinity is available, you can scale up multiple instances, each running +single processes.

+

file

+

Users should be careful when using the file backend. Each Mesop instance has their +own disk, which can be shared among multiple processes. This means cache misses will be +common if there are multiple instances and no session affinity.

+

If session affinity is available, you can scale up multiple instances, each running +multiple Mesop processes. If no session affinity is available, then you can only +vertically scale a single instance.

+

The bottleneck with this backend is the disk read/write performance. The amount of disk +space must also be carefully specified per instance in accordance with the expected user +traffic and state size.

+

You will also need to specify a directory to write the state data using +MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR.

+

SQL

+
+

NOTE: Setting up and configuring databases is out of scope of this document.

+
+

This option uses SqlAlchemy to store Mesop state sessions +in supported SQL databases, such as SQLite3 and PostgreSQL. You can also connect to +hosted options, such as GCP CloudSQL.

+

If you use SQLite3, you cannot use an in-memory database. It has to be a file. This +option has similar pros/cons as the file backend. Mesop uses the default configuration +for SQLite3, so the performance will not be optimized for Mesop's usage patterns. +SQLite3 is OK for development purposes.

+

Using a database like PostgreSQL will allow for better scalability, both vertically and +horizontally, since the database is decoupled from the Mesop server.

+

The drawback here is that this requires knowledge of the database you're using. At +minimum, you will need to create a database and a database user with the right +privileges. You will also need to create the database table, which you can create +with this script. You will need to update the CONNECTION_URI and TABLE_NAME to match +your database and settings. Also the database user for this script will need privileges +to create tables on the target database.

+
from sqlalchemy import (
+  Column,
+  DateTime,
+  LargeBinary,
+  MetaData,
+  String,
+  Table,
+  create_engine,
+)
+
+CONNECTION_URI = "your-database-connection-uri"
+# Update to "your-table-name" if you've overridden `MESOP_STATE_SESSION_BACKEND_SQL_TABLE`.
+TABLE_NAME = "mesop_state_session"
+
+db = create_engine(CONNECTION_URI)
+metadata = MetaData()
+table = Table(
+  TABLE_NAME,
+  metadata,
+  Column("token", String(23), primary_key=True),
+  Column("states", LargeBinary, nullable=False),
+  Column("created_at", DateTime, nullable=False, index=True),
+)
+
+metadata.create_all(db)
+
+

The Mesop server will raise a sqlalchemy.exc.ProgrammingError if there is a +database configuration issue.

+

By default, Mesop will use the table name mesop_state_session, but this can be +overridden using MESOP_STATE_SESSION_BACKEND_SQL_TABLE.

+

GCP Firestore

+

This options uses GCP Firestore to store +Mesop state sessions. The (default) database has a free tier that can be used for +for small demo applications with low traffic and moderate amounts of state data.

+

Since Firestore is decoupled from your Mesop server, it allows you to scale vertically +and horizontally without the considerations you'd need to make for the memory and +file backends.

+

In order to use Firestore, you will need a Google Cloud account with Firestore enabled. +Follow the instructions for creating a Firestore in Native mode database.

+

Mesop is configured to use the (default) Firestore only. The GCP project is determined +using the Application Default Credentials (ADC) which is automatically configured for +you on GCP services, such as Cloud Run.

+

For local development, you can run this command:

+
gcloud auth application-default login
+
+

If you have multiple GCP projects, you may need to update the project associated +with the ADC:

+
GCP_PROJECT=gcp-project
+gcloud config set project $GCP_PROJECT
+gcloud auth application-default set-quota-project $GCP_PROJECT
+
+

Mesop leverages Firestore's TTL policies +to delete stale state sessions. This needs to be set up using the following command, +otherwise old data will accumulate unnecessarily.

+
COLLECTION_NAME=collection_name
+gcloud firestore fields ttls update expiresAt \
+  --collection-group=$COLLECTION_NAME
+
+

By default, Mesop will use the collection name mesop_state_sessions, but this can be +overridden using MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION.

+

Default: none

+

MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR

+

This is only used when the MESOP_STATE_SESSION_BACKEND is set to file. This +parameter specifies where Mesop will read/write the session state. This means the +directory must be readable and writeable by the Mesop server processes.

+

MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION

+

This is only used when the MESOP_STATE_SESSION_BACKEND is set to firestore. This +parameter specifies which Firestore collection that Mesop will write state sessions to.

+

Default: mesop_state_sessions

+

MESOP_STATE_SESSION_BACKEND_SQL_CONNECTION_URI

+

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This +parameter specifies the database connection string. See the SqlAlchemy docs for more details.

+

Default: mesop_state_session

+

MESOP_STATE_SESSION_BACKEND_SQL_TABLE

+

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This +parameter specifies which SQL database table that Mesop will write state sessions to.

+

Default: mesop_state_session

+

Experimental configuration values

+

These configuration values are experimental and are subject to breaking change, including removal in future releases.

+

MESOP_CONCURRENT_UPDATES_ENABLED

+
+

Experimental feature

+

This is an experimental feature and is subject to breaking change. There are many bugs and edge cases to this feature.

+
+

Allows concurrent updates to state in the same session. If this is not updated, then updates are queued and processed sequentially.

+

By default, this is not enabled. You can enable this by setting it to true.

+

MESOP_WEB_SOCKETS_ENABLED

+
+

Experimental feature

+

This is an experimental feature and is subject to breaking change. Please follow https://github.com/google/mesop/issues/1028 for updates.

+
+

This uses WebSockets instead of HTTP Server-Sent Events (SSE) as the transport protocol for UI updates. If you set this environment variable to true, then MESOP_CONCURRENT_UPDATES_ENABLED will automatically be enabled as well.

+

Usage Examples

+

One-liner

+

You can specify the environment variables before the mesop command.

+
MESOP_STATE_SESSION_BACKEND=memory mesop main.py
+
+

Use a .env file

+

Mesop also supports .env files. This is nice since you don't have to keep setting +the environment variables. In addition, the variables are only set when the application +is run.

+
.env
MESOP_STATE_SESSION_BACKEND=file
+MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR=/tmp/mesop-sessions
+
+

When you run your Mesop app, the .env file will then be read.

+
mesop main.py
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/page/index.html b/api/page/index.html new file mode 100644 index 000000000..bccfe9bf7 --- /dev/null +++ b/api/page/index.html @@ -0,0 +1,2033 @@ + + + + + + + + + + + + + + + + + + + + + + + Page - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Page API

+

Overview

+

Pages allow you to build multi-page applications by decorating Python functions with me.page. To learn more, read the see multi-pages guide.

+

Examples

+

Simple, 1-page setup

+

To create a simple Mesop app, you can use me.page() like this:

+
import mesop as me
+
+@me.page()
+def foo():
+    me.text("bar")
+
+
+

NOTE: If you do not provide a path argument, then it defaults to the root path "/".

+
+

Explicit 1-page setup

+

This is the same as the above example which explicitly sets the route to "/".

+
import mesop as me
+
+@me.page(path="/")
+def foo():
+    me.text("bar")
+
+

API

+ + +
+ + +

+ page + +

+ + +
+ +

Defines a page in a Mesop application.

+

This function is used as a decorator to register a function as a page in a Mesop app.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
path +
+

The URL path for the page. Defaults to "/".

+
+

+ + TYPE: + str + + + DEFAULT: + '/' + +

+
title +
+

The title of the page. If None, a default title is generated.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
stylesheets +
+

List of stylesheet URLs to load.

+
+

+ + TYPE: + list[str] | None + + + DEFAULT: + None + +

+
security_policy +
+

The security policy for the page. If None, a default strict security policy is used.

+
+

+ + TYPE: + SecurityPolicy | None + + + DEFAULT: + None + +

+
on_load +
+

An optional event handler to be called when the page is loaded.

+
+

+ + TYPE: + OnLoadHandler | None + + + DEFAULT: + None + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ + Callable[[Callable[[], None]], Callable[[], None]] + + +
+

A decorator that registers the decorated function as a page.

+
+
+ +
+ +
+ +
+ + + +

+ SecurityPolicy + + + dataclass + + +

+ + +
+ + +

A class to represent the security policy.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
allowed_iframe_parents +
+

A list of allowed iframe parents.

+
+

+ + TYPE: + list[str] + +

+
allowed_connect_srcs +
+

A list of sites you can connect to, see MDN.

+
+

+ + TYPE: + list[str] + +

+
allowed_script_srcs +
+

A list of sites you load scripts from, see MDN.

+
+

+ + TYPE: + list[str] + +

+
dangerously_disable_trusted_types +
+

A flag to disable trusted types. +Highly recommended to not disable trusted types because +it's an important web security feature!

+
+

+ + TYPE: + bool + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ LoadEvent + + + dataclass + + +

+ + +
+ + +

Represents a page load event.

+ + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
path +
+

The path loaded

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +

on_load

+

You may want to do some sort of data-processing when a page is first loaded in a session.

+

Simple handler

+

An on_load handler is similar to a regular event handler where you can mutate state.

+
import time
+
+import mesop as me
+
+
+def fake_api():
+  yield 1
+  time.sleep(1)
+  yield 2
+  time.sleep(2)
+  yield 3
+
+
+def on_load(e: me.LoadEvent):
+  for val in fake_api():
+    me.state(State).default_values.append(val)
+    yield
+
+
+@me.page(path="/docs/on_load", on_load=on_load)
+def app():
+  me.text("onload")
+  me.text(str(me.state(State).default_values))
+
+
+@me.stateclass
+class State:
+  default_values: list[int]
+
+

Generator handler

+

The on_load handler can also be a generator function. This is useful if you need to call a slow or streaming API and want to return intermediate results before all the data has been received.

+
import time
+
+import mesop as me
+
+
+def on_load(e: me.LoadEvent):
+  state = me.state(State)
+  state.default_values.append("a")
+  yield
+  time.sleep(1)
+  state.default_values.append("b")
+  yield
+
+
+@me.page(path="/docs/on_load_generator", on_load=on_load)
+def app():
+  me.text("onload")
+  me.text(str(me.state(State).default_values))
+
+
+@me.stateclass
+class State:
+  default_values: list[str]
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/query-params/index.html b/api/query-params/index.html new file mode 100644 index 000000000..30d0723d0 --- /dev/null +++ b/api/query-params/index.html @@ -0,0 +1,1697 @@ + + + + + + + + + + + + + + + + + + + + + + + Query params - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Query Params API

+

Overview

+

Query params, also sometimes called query string, provide a way to manage state in the URLs. They are useful for providing deep-links into your Mesop app.

+

Example

+

Here's a simple working example that shows how you can read and write query params.

+
@me.page(path="/examples/query_params/page_2")
+def page_2():
+  me.text(f"query_params={me.query_params}")
+  me.button("Add query param", on_click=add_query_param)
+  me.button("Navigate", on_click=navigate)
+
+def add_query_param(e: me.ClickEvent):
+  me.query_params["key"] = "value"
+
+def navigate(e: me.ClickEvent):
+  me.navigate("/examples/query_params", query_params=me.query_params)
+
+

Usage

+

You can use query parameters from me.query_params, which has a dictionary-like interface, where the key is the parameter name and value is the parameter value.

+

Get a query param value

+

value: str = me.query_params['param_name']
+
+This will raise a KeyError if the parameter doesn't exist. You can use in to check whether a key exists in me.query_params:

+
if 'key' in me.query_params:
+    print(me.query_params['key'])
+
+
+Repeated query params +

If a query param key is repeated, then you will get the first value. If you want all the values use get_all.

+
+

Get all values

+

To get all the values for a particular query parameter key, you can use me.query_params.get_all, which returns a sequence of parameter values (currently implemented as a tuple).

+
all_values = me.query_params.get_all('param_name')
+
+

Iterate

+
for key in query_params:
+  value = query_params[key]
+
+

Set query param

+
query_params['new_param'] = 'value'
+
+

Set repeated query param

+
query_params['repeated_param'] = ['value1', 'value2']
+
+

Delete

+
del query_params['param_to_delete']
+
+

Patterns

+ +

Here's an example of how to navigate to a new page with query parameters:

+
def click_navigate_button(e: me.ClickEvent):
+    me.query_params['q'] = "value"
+    me.navigate('/search', query_params=me.query_params)
+
+ +

You can also navigate by passing in a dictionary to query_params parameter for me.navigate if you do not want to keep the existing query parameters.

+
def click_navigate_button(e: me.ClickEvent):
+    me.navigate('/search', query_params={"q": "value})
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/style/index.html b/api/style/index.html new file mode 100644 index 000000000..3ee1cc435 --- /dev/null +++ b/api/style/index.html @@ -0,0 +1,3792 @@ + + + + + + + + + + + + + + + + + + + + + + + Style - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Style

+ +

Overview

+

Mesop provides a Python API that wraps the browser's native CSS style API.

+

API

+ + +
+ + + +

+ Style + + + dataclass + + +

+ + +
+ + +

Represents the style configuration for a UI component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
align_content +
+

Aligns the flexible container's items on the cross-axis. See MDN doc.

+
+

+ + TYPE: + ContentAlignmentValues | None + +

+
align_items +
+

Specifies the default alignment for items inside a flexible container. See MDN doc.

+
+

+ + TYPE: + ItemAlignmentValues | None + +

+
align_self +
+

Overrides a grid or flex item's align-items value. In Grid, it aligns the item inside the grid area. In Flexbox, it aligns the item on the cross axis. See MDN doc.

+
+

+ + TYPE: + ItemAlignmentValues | None + +

+
aspect_ratio +
+

Specifies the desired width-to-height ratio of a component. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
backdrop_filter +
+

Applies a CSS filter to the backdrop of the component. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
background +
+

Sets the background color or image of the component. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
border +
+

Defines the border properties for each side of the component. See MDN doc.

+
+

+ + TYPE: + Border | None + +

+
border_radius +
+

Defines the border radius. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
bottom +
+

Helps set vertical position of a positioned element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
box_shadow +
+

Defines the box shadow. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
box_sizing +
+

Defines the box sizing. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
color +
+

Sets the color of the text inside the component. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
column_gap +
+

Sets the gap between columns. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
columns +
+

Specifies the number of columns in a multi-column element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
cursor +
+

Sets the mouse cursor. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
display +
+

Defines the display type of the component. See MDN doc.

+
+

+ + TYPE: + Literal['block', 'inline', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'none', 'contents'] | None + +

+
flex +
+

Defines the flexbox layout using a shorthand property. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
flex_basis +
+

Specifies the initial length of a flexible item. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
flex_direction +
+

Establishes the main-axis, thus defining the direction flex items are placed in the flex container. See MDN doc.

+
+

+ + TYPE: + Literal['row', 'row-reverse', 'column', 'column-reverse'] | None + +

+
flex_grow +
+

Defines the ability for a flex item to grow if necessary. See MDN doc.

+
+

+ + TYPE: + int | None + +

+
flex_shrink +
+

Defines the ability for a flex item to shrink if necessary. See MDN doc.

+
+

+ + TYPE: + int | None + +

+
flex_wrap +
+

Allows flex items to wrap onto multiple lines. See MDN doc.

+
+

+ + TYPE: + Literal['nowrap', 'wrap', 'wrap-reverse'] | None + +

+
font_family +
+

Specifies the font family. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
font_size +
+

Sets the size of the font. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
font_style +
+

Specifies the font style for text. See MDN doc.

+
+

+ + TYPE: + Literal['italic', 'normal'] | None + +

+
font_weight +
+

Sets the weight (or boldness) of the font. See MDN doc.

+
+

+ + TYPE: + Literal['bold', 'normal', 'medium', 100, 200, 300, 400, 500, 600, 700, 800, 900] | None + +

+
gap +
+

Sets the gap. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
grid_area +
+

Sets the grid area. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_auto_columns +
+

CSS property specifies the size of an implicitly-created grid column track or pattern of tracks. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_auto_flow +
+

CSS property controls how the auto-placement algorithm works, specifying exactly how auto-placed items get flowed into the grid. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_auto_rows +
+

CSS property specifies the size of an implicitly-created grid row track or pattern of tracks. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_column +
+

CSS shorthand property specifies a grid item's size and location within a grid column. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_column_start +
+

Sets the grid column start. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
grid_column_end +
+

Sets the grid column end. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
grid_row +
+

CSS shorthand property specifies a grid item's size and location within a grid row. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_row_start +
+

Sets the grid row start. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
grid_row_end +
+

Sets the grid row end. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
grid_template_areas +
+

Sets the grid template areas; each element is a row. See MDN doc.

+
+

+ + TYPE: + list[str] | None + +

+
grid_template_columns +
+

Sets the grid template columns. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
grid_template_rows +
+

Sets the grid template rows. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
height +
+

Sets the height of the component. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
justify_content +
+

Aligns the flexible container's items on the main-axis. See MDN doc.

+
+

+ + TYPE: + ContentAlignmentValues | None + +

+
justify_items +
+

Defines the default justify-self for all items of the box, giving them all a default way of justifying each box along the appropriate axis. See MDN doc.

+
+

+ + TYPE: + ItemJustifyValues | None + +

+
justify_self +
+

Sets the way a box is justified inside its alignment container along the appropriate axis. See MDN doc.

+
+

+ + TYPE: + ItemJustifyValues | None + +

+
left +
+

Helps set horizontal position of a positioned element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
letter_spacing +
+

Increases or decreases the space between characters in text. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
line +
+

Set the line height (relative to the font size). See MDN doc.

+
+

+ + TYPE: + height + +

+
margin +
+

Sets the margin space required on each side of an element. See MDN doc.

+
+

+ + TYPE: + Margin | None + +

+
max_height +
+

Sets the maximum height of an element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
max_width +
+

Sets the maximum width of an element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
min_height +
+

Sets the minimum height of an element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
min_width +
+

Sets the minimum width of an element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
object_fit +
+

Specifies how an image or video should be resized to fit its container. See MDN doc.

+
+

+ + TYPE: + ObjectFitValues | None + +

+
opacity +
+

Sets the opacity property. See MDN doc.

+
+

+ + TYPE: + float | str | None + +

+
outline +
+

Sets the outline property. Note: input component has default browser stylings. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
overflow_wrap +
+

Specifies how long text can be broken up by new lines to prevent overflowing. See MDN doc.

+
+

+ + TYPE: + OverflowWrapValues | None + +

+
overflow +
+

Specifies the handling of overflow in the horizontal and vertical direction. See MDN doc.

+
+

+ + TYPE: + OverflowValues | None + +

+
overflow_x +
+

Specifies the handling of overflow in the horizontal direction. See MDN doc.

+
+

+ + TYPE: + OverflowValues | None + +

+
overflow_y +
+

Specifies the handling of overflow in the vertical direction. See MDN doc.

+
+

+ + TYPE: + OverflowValues | None + +

+
padding +
+

Sets the padding space required on each side of an element. See MDN doc.

+
+

+ + TYPE: + Padding | None + +

+
place_items +
+

The CSS place-items shorthand property allows you to align items along both the block and inline directions at once. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
pointer_events +
+

Sets under what circumstances (if any) a particular graphic element can become the target of pointer events. See MDN doc.

+
+

+ + TYPE: + PointerEventsValues | None + +

+
position +
+

Specifies the type of positioning method used for an element (static, relative, absolute, fixed, or sticky). See MDN doc.

+
+

+ + TYPE: + Literal['static', 'relative', 'absolute', 'fixed', 'sticky'] | None + +

+
right +
+

Helps set horizontal position of a positioned element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
rotate +
+

Allows you to specify rotation transforms individually and independently of the transform property. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
row_gap +
+

Sets the gap between rows. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
text_align +
+

Specifies the horizontal alignment of text in an element. See MDN doc.

+
+

+ + TYPE: + Literal['start', 'end', 'left', 'right', 'center'] | None + +

+
text_decoration +
+

Specifies the decoration added to text. See MDN doc.

+
+

+ + TYPE: + Literal['underline', 'none'] | None + +

+
text_overflow +
+

Specifies how overflowed content that is not displayed should be signaled to the user. See MDN doc.

+
+

+ + TYPE: + Literal['ellipsis', 'clip'] | None + +

+
text_shadow +
+

Specifies the shadow effect applied to text. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
text_transform +
+

Specifies the transformation applied to text. See MDN doc.

+
+

+ + TYPE: + Literal['uppercase', 'lowercase', 'capitalize', 'none', 'full-width', 'full-size-kana'] | None + +

+
top +
+

Helps set vertical position of a positioned element. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
transform +
+

Lets you rotate, scale, skew, or translate an element. It modifies the coordinate space of the CSS visual formatting model. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
transition +
+

Specifies the transition effect. See MDN doc.

+
+

+ + TYPE: + str | None + +

+
vertical_align +
+

Specifies the vertical alignment of an element. See MDN doc.

+
+

+ + TYPE: + Literal['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'initial', 'inherit', 'revert', 'revert-layer', 'unset'] | None + +

+
visibility +
+

Sets the visibility property. See MDN doc.

+
+

+ + TYPE: + Literal['visible', 'hidden', 'collapse', 'inherit', 'initial', 'revert', 'revert-layer', 'unset'] | None + +

+
white_space +
+

Specifies how white space inside an element is handled. See MDN doc.

+
+

+ + TYPE: + Literal['normal', 'nowrap', 'pre', 'pre-wrap', 'pre-line', 'break-spaces'] | None + +

+
width +
+

Sets the width of the component. See MDN doc.

+
+

+ + TYPE: + int | str | None + +

+
word_wrap +
+

Specifies how long text can be broken up by new lines to prevent overflowing. See MDN doc.

+
+

+ + TYPE: + Literal['normal', 'break-word', 'anywhere'] | None + +

+
z-index +
+

Sets the z-index of the component. See MDN doc.

+
+

+ + TYPE: + Literal['normal', 'break-word', 'anywhere'] | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ Border + + + dataclass + + +

+ + +
+ + +

Defines the border styles for each side of a UI component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
top +
+

Style for the top border.

+
+

+ + TYPE: + BorderSide | None + +

+
right +
+

Style for the right border.

+
+

+ + TYPE: + BorderSide | None + +

+
bottom +
+

Style for the bottom border.

+
+

+ + TYPE: + BorderSide | None + +

+
left +
+

Style for the left border.

+
+

+ + TYPE: + BorderSide | None + +

+
+ + + + +
+ + + + + + + + + +
+ + +

+ all + + + staticmethod + + +

+ + +
+ +

Creates a Border instance with all sides having the same style.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
value +
+

The style to apply to all sides of the border.

+
+

+ + TYPE: + BorderSide + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Border + +
+

A new Border instance with the specified style applied to all sides.

+
+

+ + TYPE: + Border + +

+
+ +
+ +
+ +
+ + +

+ symmetric + + + staticmethod + + +

+ + +
+ +

Creates a Border instance with symmetric styles for vertical and horizontal sides.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
vertical +
+

The style to apply to the top and bottom sides of the border.

+
+

+ + TYPE: + BorderSide | None + + + DEFAULT: + None + +

+
horizontal +
+

The style to apply to the right and left sides of the border.

+
+

+ + TYPE: + BorderSide | None + + + DEFAULT: + None + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Border + +
+

A new Border instance with the specified styles applied symmetrically.

+
+

+ + TYPE: + Border + +

+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ BorderSide + + + dataclass + + +

+ + +
+ + +

Represents the style of a single side of a border in a UI component.

+ + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
width +
+

The width of the border. Can be specified as an integer value representing pixels, + a string with a unit (e.g., '2em'), or None for no width.

+
+

+ + TYPE: + int | str | None + +

+
color +
+

The color of the border, represented as a string. This can be any valid CSS color value, + or None for no color.

+
+

+ + TYPE: + str | None + +

+
style +
+

The style of the border. See https://developer.mozilla.org/en-US/docs/Web/CSS/border-style

+
+

+ + TYPE: + Literal['none', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset', 'hidden'] | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ Margin + + + dataclass + + +

+ + +
+

+ Bases: _EdgeInsets

+ + +

Defines the margin space around a UI component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
top +
+

Top margin (note: 2 is the same as 2px)

+
+

+ + TYPE: + int | str | None + +

+
right +
+

Right margin

+
+

+ + TYPE: + int | str | None + +

+
bottom +
+

Bottom margin

+
+

+ + TYPE: + int | str | None + +

+
left +
+

Left margin

+
+

+ + TYPE: + int | str | None + +

+
+ + + + +
+ + + + + + + + + +
+ + +

+ all + + + staticmethod + + +

+ + +
+ +

Creates a Margin instance with the same value for all sides.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
value +
+

The value to apply to all sides of the margin. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Margin + +
+

A new Margin instance with the specified value applied to all sides.

+
+

+ + TYPE: + Margin + +

+
+ +
+ +
+ +
+ + +

+ symmetric + + + staticmethod + + +

+ + +
+ +

Creates a Margin instance with symmetric values for vertical and horizontal sides.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
vertical +
+

The value to apply to the top and bottom sides of the margin. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str | None + + + DEFAULT: + None + +

+
horizontal +
+

The value to apply to the right and left sides of the margin. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str | None + + + DEFAULT: + None + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Margin + +
+

A new Margin instance with the specified values applied to the vertical and horizontal sides.

+
+

+ + TYPE: + Margin + +

+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +

+ Padding + + + dataclass + + +

+ + +
+

+ Bases: _EdgeInsets

+ + +

Defines the padding space around a UI component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
top +
+

Top padding (note: 2 is the same as 2px)

+
+

+ + TYPE: + int | str | None + +

+
right +
+

Right padding

+
+

+ + TYPE: + int | str | None + +

+
bottom +
+

Bottom padding

+
+

+ + TYPE: + int | str | None + +

+
left +
+

Left padding

+
+

+ + TYPE: + int | str | None + +

+
+ + + + +
+ + + + + + + + + +
+ + +

+ all + + + staticmethod + + +

+ + +
+ +

Creates a Padding instance with the same value for all sides.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
value +
+

The value to apply to all sides of the padding. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Padding + +
+

A new Padding instance with the specified value applied to all sides.

+
+

+ + TYPE: + Padding + +

+
+ +
+ +
+ +
+ + +

+ symmetric + + + staticmethod + + +

+ + +
+ +

Creates a Padding instance with symmetric values for vertical and horizontal sides.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
vertical +
+

The value to apply to the top and bottom sides of the padding. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str | None + + + DEFAULT: + None + +

+
horizontal +
+

The value to apply to the right and left sides of the padding. Can be an integer (pixel value) or a string.

+
+

+ + TYPE: + int | str | None + + + DEFAULT: + None + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Padding + +
+

A new Padding instance with the specified values applied to the vertical and horizontal sides.

+
+

+ + TYPE: + Padding + +

+
+ +
+ +
+ + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/api/viewport-size/index.html b/api/viewport-size/index.html new file mode 100644 index 000000000..0dffe130f --- /dev/null +++ b/api/viewport-size/index.html @@ -0,0 +1,1711 @@ + + + + + + + + + + + + + + + + + + + + + + + Viewport Size - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + Skip to content + + +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Viewport size

+

Overview

+

The viewport size API allows you to access the current viewport size. This can be useful for creating responsive and adaptive designs that are suitable for the user's screen size.

+

Examples

+

Responsive Design

+

Responsive design is having a single fluid layout that adapts to all screen sizes.

+

You can use the viewport size to dynamically set the property of a style. This can be useful if you want to fit two boxes in a row for larger screens (e.g. desktop) and a single box for smaller screens (e.g. mobile) as shown in the example below:

+
import mesop as me
+
+@me.page()
+def page():
+    if me.viewport_size().width > 640:
+        width = me.viewport_size().width / 2
+    else:
+        width = me.viewport_size().width
+    for i in range(8):
+      me.box(style=me.Style(width=width))
+
+
+

Tip: Responsive design tends to take less work and is usually a good starting point.

+
+

Adaptive Design

+

Adaptive design is having multiple fixed layouts for specific device categories at specific breakpoints, typically viewport width.

+

For example, oftentimes you will hide the nav component on a mobile device and instead show a hamburger menu, while for a larger device you will always show the nav component on the left side.

+
import mesop as me
+
+@me.page()
+def page():
+    if me.viewport_size().width > 480:
+        nav_component()
+        body()
+    else:
+        body(show_menu_button=True)
+
+
+

Tip: Adaptive design tends to take more work and is best for optimizing complex mobile and desktop experiences.

+
+

API

+ + +
+ + +

+ viewport_size + +

+ + +
+ +

Returns the current viewport size.

+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ Size + +
+

The current viewport size.

+
+

+ + TYPE: + Size + +

+
+ +
+ +
+ +
+ + + +

+ Size + + + dataclass + + +

+ + +
+ + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
width +
+

The width of the viewport in pixels.

+
+

+ + TYPE: + int + +

+
height +
+

The height of the viewport in pixels.

+
+

+ + TYPE: + int + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/assets/_mkdocstrings.css b/assets/_mkdocstrings.css new file mode 100644 index 000000000..85449ec79 --- /dev/null +++ b/assets/_mkdocstrings.css @@ -0,0 +1,119 @@ + +/* Avoid breaking parameter names, etc. in table cells. */ +.doc-contents td code { + word-break: normal !important; +} + +/* No line break before first paragraph of descriptions. */ +.doc-md-description, +.doc-md-description>p:first-child { + display: inline; +} + +/* Max width for docstring sections tables. */ +.doc .md-typeset__table, +.doc .md-typeset__table table { + display: table !important; + width: 100%; +} + +.doc .md-typeset__table tr { + display: table-row; +} + +/* Defaults in Spacy table style. */ +.doc-param-default { + float: right; +} + +/* Backward-compatibility: docstring section titles in bold. */ +.doc-section-title { + font-weight: bold; +} + +/* Symbols in Navigation and ToC. */ +:root, +[data-md-color-scheme="default"] { + --doc-symbol-attribute-fg-color: #953800; + --doc-symbol-function-fg-color: #8250df; + --doc-symbol-method-fg-color: #8250df; + --doc-symbol-class-fg-color: #0550ae; + --doc-symbol-module-fg-color: #5cad0f; + + --doc-symbol-attribute-bg-color: #9538001a; + --doc-symbol-function-bg-color: #8250df1a; + --doc-symbol-method-bg-color: #8250df1a; + --doc-symbol-class-bg-color: #0550ae1a; + --doc-symbol-module-bg-color: #5cad0f1a; +} + +[data-md-color-scheme="slate"] { + --doc-symbol-attribute-fg-color: #ffa657; + --doc-symbol-function-fg-color: #d2a8ff; + --doc-symbol-method-fg-color: #d2a8ff; + --doc-symbol-class-fg-color: #79c0ff; + --doc-symbol-module-fg-color: #baff79; + + --doc-symbol-attribute-bg-color: #ffa6571a; + --doc-symbol-function-bg-color: #d2a8ff1a; + --doc-symbol-method-bg-color: #d2a8ff1a; + --doc-symbol-class-bg-color: #79c0ff1a; + --doc-symbol-module-bg-color: #baff791a; +} + +code.doc-symbol { + border-radius: .1rem; + font-size: .85em; + padding: 0 .3em; + font-weight: bold; +} + +code.doc-symbol-attribute { + color: var(--doc-symbol-attribute-fg-color); + background-color: var(--doc-symbol-attribute-bg-color); +} + +code.doc-symbol-attribute::after { + content: "attr"; +} + +code.doc-symbol-function { + color: var(--doc-symbol-function-fg-color); + background-color: var(--doc-symbol-function-bg-color); +} + +code.doc-symbol-function::after { + content: "func"; +} + +code.doc-symbol-method { + color: var(--doc-symbol-method-fg-color); + background-color: var(--doc-symbol-method-bg-color); +} + +code.doc-symbol-method::after { + content: "meth"; +} + +code.doc-symbol-class { + color: var(--doc-symbol-class-fg-color); + background-color: var(--doc-symbol-class-bg-color); +} + +code.doc-symbol-class::after { + content: "class"; +} + +code.doc-symbol-module { + color: var(--doc-symbol-module-fg-color); + background-color: var(--doc-symbol-module-bg-color); +} + +code.doc-symbol-module::after { + content: "mod"; +} + +.doc-signature .autorefs { + color: inherit; + border-bottom: 1px dotted currentcolor; +} diff --git a/assets/codespaces/create.png b/assets/codespaces/create.png new file mode 100644 index 000000000..8661f14bf Binary files /dev/null and b/assets/codespaces/create.png differ diff --git a/assets/codespaces/ignore-error-cli.png b/assets/codespaces/ignore-error-cli.png new file mode 100644 index 000000000..cf9ddfdf6 Binary files /dev/null and b/assets/codespaces/ignore-error-cli.png differ diff --git a/assets/codespaces/post-create-command.png b/assets/codespaces/post-create-command.png new file mode 100644 index 000000000..fa2075c23 Binary files /dev/null and b/assets/codespaces/post-create-command.png differ diff --git a/assets/codespaces/set-env.png b/assets/codespaces/set-env.png new file mode 100644 index 000000000..f0a6c8ec5 Binary files /dev/null and b/assets/codespaces/set-env.png differ diff --git a/assets/codespaces/view-mesop.png b/assets/codespaces/view-mesop.png new file mode 100644 index 000000000..72cad244e Binary files /dev/null and b/assets/codespaces/view-mesop.png differ diff --git a/assets/colab.svg b/assets/colab.svg new file mode 100644 index 000000000..e5830d533 --- /dev/null +++ b/assets/colab.svg @@ -0,0 +1 @@ + Open in ColabOpen in Colab diff --git a/assets/csp-message.webp b/assets/csp-message.webp new file mode 100644 index 000000000..4a52d81f8 Binary files /dev/null and b/assets/csp-message.webp differ diff --git a/assets/editor-v1.png b/assets/editor-v1.png new file mode 100644 index 000000000..f54858110 Binary files /dev/null and b/assets/editor-v1.png differ diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 000000000..b36c85f39 Binary files /dev/null and b/assets/favicon.ico differ diff --git a/assets/hf/create-hf-space-2.png b/assets/hf/create-hf-space-2.png new file mode 100644 index 000000000..dc8f6970e Binary files /dev/null and b/assets/hf/create-hf-space-2.png differ diff --git a/assets/hf/create-hf-space.png b/assets/hf/create-hf-space.png new file mode 100644 index 000000000..f3a285644 Binary files /dev/null and b/assets/hf/create-hf-space.png differ diff --git a/assets/hf/deployed-hf-space.png b/assets/hf/deployed-hf-space.png new file mode 100644 index 000000000..3038e8b97 Binary files /dev/null and b/assets/hf/deployed-hf-space.png differ diff --git a/assets/hf/example.Dockerfile b/assets/hf/example.Dockerfile new file mode 100644 index 000000000..d420a4374 --- /dev/null +++ b/assets/hf/example.Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.10.15-bullseye + +RUN apt-get update && \ + apt-get install -y \ + # General dependencies + locales \ + locales-all && \ + # Clean local repository of package files since they won't be needed anymore. + # Make sure this line is called after all apt-get update/install commands have + # run. + apt-get clean && \ + # Also delete the index files which we also don't need anymore. + rm -rf /var/lib/apt/lists/* + +ENV LC_ALL en_US.UTF-8 +ENV LANG en_US.UTF-8 +ENV LANGUAGE en_US.UTF-8 + +# Install dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt + +# Create non-root user +RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop +USER mesop + +# Add app code here +COPY . /srv/mesop-app +WORKDIR /srv/mesop-app + +# Run Mesop through gunicorn. Should be available at localhost:8080 +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:me"] diff --git a/assets/hf/select-docker.png b/assets/hf/select-docker.png new file mode 100644 index 000000000..e6bda91cb Binary files /dev/null and b/assets/hf/select-docker.png differ diff --git a/assets/hot-reload.gif b/assets/hot-reload.gif new file mode 100644 index 000000000..bb62163d1 Binary files /dev/null and b/assets/hot-reload.gif differ diff --git a/assets/ide-support.gif b/assets/ide-support.gif new file mode 100644 index 000000000..2f050c352 Binary files /dev/null and b/assets/ide-support.gif differ diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 000000000..1cf13b9f9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.ad660dcc.min.js b/assets/javascripts/bundle.ad660dcc.min.js new file mode 100644 index 000000000..0ffc0460a --- /dev/null +++ b/assets/javascripts/bundle.ad660dcc.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Fi=Object.create;var gr=Object.defineProperty;var ji=Object.getOwnPropertyDescriptor;var Wi=Object.getOwnPropertyNames,Dt=Object.getOwnPropertySymbols,Ui=Object.getPrototypeOf,xr=Object.prototype.hasOwnProperty,no=Object.prototype.propertyIsEnumerable;var oo=(e,t,r)=>t in e?gr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,R=(e,t)=>{for(var r in t||(t={}))xr.call(t,r)&&oo(e,r,t[r]);if(Dt)for(var r of Dt(t))no.call(t,r)&&oo(e,r,t[r]);return e};var io=(e,t)=>{var r={};for(var o in e)xr.call(e,o)&&t.indexOf(o)<0&&(r[o]=e[o]);if(e!=null&&Dt)for(var o of Dt(e))t.indexOf(o)<0&&no.call(e,o)&&(r[o]=e[o]);return r};var yr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Di=(e,t,r,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Wi(t))!xr.call(e,n)&&n!==r&&gr(e,n,{get:()=>t[n],enumerable:!(o=ji(t,n))||o.enumerable});return e};var Vt=(e,t,r)=>(r=e!=null?Fi(Ui(e)):{},Di(t||!e||!e.__esModule?gr(r,"default",{value:e,enumerable:!0}):r,e));var ao=(e,t,r)=>new Promise((o,n)=>{var i=p=>{try{s(r.next(p))}catch(c){n(c)}},a=p=>{try{s(r.throw(p))}catch(c){n(c)}},s=p=>p.done?o(p.value):Promise.resolve(p.value).then(i,a);s((r=r.apply(e,t)).next())});var co=yr((Er,so)=>{(function(e,t){typeof Er=="object"&&typeof so!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(Er,function(){"use strict";function e(r){var o=!0,n=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(H){return!!(H&&H!==document&&H.nodeName!=="HTML"&&H.nodeName!=="BODY"&&"classList"in H&&"contains"in H.classList)}function p(H){var mt=H.type,ze=H.tagName;return!!(ze==="INPUT"&&a[mt]&&!H.readOnly||ze==="TEXTAREA"&&!H.readOnly||H.isContentEditable)}function c(H){H.classList.contains("focus-visible")||(H.classList.add("focus-visible"),H.setAttribute("data-focus-visible-added",""))}function l(H){H.hasAttribute("data-focus-visible-added")&&(H.classList.remove("focus-visible"),H.removeAttribute("data-focus-visible-added"))}function f(H){H.metaKey||H.altKey||H.ctrlKey||(s(r.activeElement)&&c(r.activeElement),o=!0)}function u(H){o=!1}function h(H){s(H.target)&&(o||p(H.target))&&c(H.target)}function w(H){s(H.target)&&(H.target.classList.contains("focus-visible")||H.target.hasAttribute("data-focus-visible-added"))&&(n=!0,window.clearTimeout(i),i=window.setTimeout(function(){n=!1},100),l(H.target))}function A(H){document.visibilityState==="hidden"&&(n&&(o=!0),te())}function te(){document.addEventListener("mousemove",J),document.addEventListener("mousedown",J),document.addEventListener("mouseup",J),document.addEventListener("pointermove",J),document.addEventListener("pointerdown",J),document.addEventListener("pointerup",J),document.addEventListener("touchmove",J),document.addEventListener("touchstart",J),document.addEventListener("touchend",J)}function ie(){document.removeEventListener("mousemove",J),document.removeEventListener("mousedown",J),document.removeEventListener("mouseup",J),document.removeEventListener("pointermove",J),document.removeEventListener("pointerdown",J),document.removeEventListener("pointerup",J),document.removeEventListener("touchmove",J),document.removeEventListener("touchstart",J),document.removeEventListener("touchend",J)}function J(H){H.target.nodeName&&H.target.nodeName.toLowerCase()==="html"||(o=!1,ie())}document.addEventListener("keydown",f,!0),document.addEventListener("mousedown",u,!0),document.addEventListener("pointerdown",u,!0),document.addEventListener("touchstart",u,!0),document.addEventListener("visibilitychange",A,!0),te(),r.addEventListener("focus",h,!0),r.addEventListener("blur",w,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var Yr=yr((Rt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Rt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Rt=="object"?Rt.ClipboardJS=r():t.ClipboardJS=r()})(Rt,function(){return function(){var e={686:function(o,n,i){"use strict";i.d(n,{default:function(){return Ii}});var a=i(279),s=i.n(a),p=i(370),c=i.n(p),l=i(817),f=i.n(l);function u(V){try{return document.execCommand(V)}catch(_){return!1}}var h=function(_){var O=f()(_);return u("cut"),O},w=h;function A(V){var _=document.documentElement.getAttribute("dir")==="rtl",O=document.createElement("textarea");O.style.fontSize="12pt",O.style.border="0",O.style.padding="0",O.style.margin="0",O.style.position="absolute",O.style[_?"right":"left"]="-9999px";var j=window.pageYOffset||document.documentElement.scrollTop;return O.style.top="".concat(j,"px"),O.setAttribute("readonly",""),O.value=V,O}var te=function(_,O){var j=A(_);O.container.appendChild(j);var D=f()(j);return u("copy"),j.remove(),D},ie=function(_){var O=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},j="";return typeof _=="string"?j=te(_,O):_ instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(_==null?void 0:_.type)?j=te(_.value,O):(j=f()(_),u("copy")),j},J=ie;function H(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?H=function(O){return typeof O}:H=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},H(V)}var mt=function(){var _=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},O=_.action,j=O===void 0?"copy":O,D=_.container,Y=_.target,ke=_.text;if(j!=="copy"&&j!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(Y!==void 0)if(Y&&H(Y)==="object"&&Y.nodeType===1){if(j==="copy"&&Y.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(j==="cut"&&(Y.hasAttribute("readonly")||Y.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(ke)return J(ke,{container:D});if(Y)return j==="cut"?w(Y):J(Y,{container:D})},ze=mt;function Ie(V){"@babel/helpers - typeof";return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(O){return typeof O}:Ie=function(O){return O&&typeof Symbol=="function"&&O.constructor===Symbol&&O!==Symbol.prototype?"symbol":typeof O},Ie(V)}function _i(V,_){if(!(V instanceof _))throw new TypeError("Cannot call a class as a function")}function ro(V,_){for(var O=0;O<_.length;O++){var j=_[O];j.enumerable=j.enumerable||!1,j.configurable=!0,"value"in j&&(j.writable=!0),Object.defineProperty(V,j.key,j)}}function Ai(V,_,O){return _&&ro(V.prototype,_),O&&ro(V,O),V}function Ci(V,_){if(typeof _!="function"&&_!==null)throw new TypeError("Super expression must either be null or a function");V.prototype=Object.create(_&&_.prototype,{constructor:{value:V,writable:!0,configurable:!0}}),_&&br(V,_)}function br(V,_){return br=Object.setPrototypeOf||function(j,D){return j.__proto__=D,j},br(V,_)}function Hi(V){var _=Pi();return function(){var j=Wt(V),D;if(_){var Y=Wt(this).constructor;D=Reflect.construct(j,arguments,Y)}else D=j.apply(this,arguments);return ki(this,D)}}function ki(V,_){return _&&(Ie(_)==="object"||typeof _=="function")?_:$i(V)}function $i(V){if(V===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return V}function Pi(){if(typeof Reflect=="undefined"||!Reflect.construct||Reflect.construct.sham)return!1;if(typeof Proxy=="function")return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],function(){})),!0}catch(V){return!1}}function Wt(V){return Wt=Object.setPrototypeOf?Object.getPrototypeOf:function(O){return O.__proto__||Object.getPrototypeOf(O)},Wt(V)}function vr(V,_){var O="data-clipboard-".concat(V);if(_.hasAttribute(O))return _.getAttribute(O)}var Ri=function(V){Ci(O,V);var _=Hi(O);function O(j,D){var Y;return _i(this,O),Y=_.call(this),Y.resolveOptions(D),Y.listenClick(j),Y}return Ai(O,[{key:"resolveOptions",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof D.action=="function"?D.action:this.defaultAction,this.target=typeof D.target=="function"?D.target:this.defaultTarget,this.text=typeof D.text=="function"?D.text:this.defaultText,this.container=Ie(D.container)==="object"?D.container:document.body}},{key:"listenClick",value:function(D){var Y=this;this.listener=c()(D,"click",function(ke){return Y.onClick(ke)})}},{key:"onClick",value:function(D){var Y=D.delegateTarget||D.currentTarget,ke=this.action(Y)||"copy",Ut=ze({action:ke,container:this.container,target:this.target(Y),text:this.text(Y)});this.emit(Ut?"success":"error",{action:ke,text:Ut,trigger:Y,clearSelection:function(){Y&&Y.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(D){return vr("action",D)}},{key:"defaultTarget",value:function(D){var Y=vr("target",D);if(Y)return document.querySelector(Y)}},{key:"defaultText",value:function(D){return vr("text",D)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(D){var Y=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return J(D,Y)}},{key:"cut",value:function(D){return w(D)}},{key:"isSupported",value:function(){var D=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],Y=typeof D=="string"?[D]:D,ke=!!document.queryCommandSupported;return Y.forEach(function(Ut){ke=ke&&!!document.queryCommandSupported(Ut)}),ke}}]),O}(s()),Ii=Ri},828:function(o){var n=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,p){for(;s&&s.nodeType!==n;){if(typeof s.matches=="function"&&s.matches(p))return s;s=s.parentNode}}o.exports=a},438:function(o,n,i){var a=i(828);function s(l,f,u,h,w){var A=c.apply(this,arguments);return l.addEventListener(u,A,w),{destroy:function(){l.removeEventListener(u,A,w)}}}function p(l,f,u,h,w){return typeof l.addEventListener=="function"?s.apply(null,arguments):typeof u=="function"?s.bind(null,document).apply(null,arguments):(typeof l=="string"&&(l=document.querySelectorAll(l)),Array.prototype.map.call(l,function(A){return s(A,f,u,h,w)}))}function c(l,f,u,h){return function(w){w.delegateTarget=a(w.target,f),w.delegateTarget&&h.call(l,w)}}o.exports=p},879:function(o,n){n.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},n.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||n.node(i[0]))},n.string=function(i){return typeof i=="string"||i instanceof String},n.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(o,n,i){var a=i(879),s=i(438);function p(u,h,w){if(!u&&!h&&!w)throw new Error("Missing required arguments");if(!a.string(h))throw new TypeError("Second argument must be a String");if(!a.fn(w))throw new TypeError("Third argument must be a Function");if(a.node(u))return c(u,h,w);if(a.nodeList(u))return l(u,h,w);if(a.string(u))return f(u,h,w);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(u,h,w){return u.addEventListener(h,w),{destroy:function(){u.removeEventListener(h,w)}}}function l(u,h,w){return Array.prototype.forEach.call(u,function(A){A.addEventListener(h,w)}),{destroy:function(){Array.prototype.forEach.call(u,function(A){A.removeEventListener(h,w)})}}}function f(u,h,w){return s(document.body,u,h,w)}o.exports=p},817:function(o){function n(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var p=window.getSelection(),c=document.createRange();c.selectNodeContents(i),p.removeAllRanges(),p.addRange(c),a=p.toString()}return a}o.exports=n},279:function(o){function n(){}n.prototype={on:function(i,a,s){var p=this.e||(this.e={});return(p[i]||(p[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var p=this;function c(){p.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),p=0,c=s.length;for(p;p{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var ts=/["'&<>]/;ei.exports=rs;function rs(e){var t=""+e,r=ts.exec(t);if(!r)return t;var o,n="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[o++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function N(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var o=r.call(e),n,i=[],a;try{for(;(t===void 0||t-- >0)&&!(n=o.next()).done;)i.push(n.value)}catch(s){a={error:s}}finally{try{n&&!n.done&&(r=o.return)&&r.call(o)}finally{if(a)throw a.error}}return i}function q(e,t,r){if(r||arguments.length===2)for(var o=0,n=t.length,i;o1||s(u,h)})})}function s(u,h){try{p(o[u](h))}catch(w){f(i[0][3],w)}}function p(u){u.value instanceof nt?Promise.resolve(u.value.v).then(c,l):f(i[0][2],u)}function c(u){s("next",u)}function l(u){s("throw",u)}function f(u,h){u(h),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mo(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof de=="function"?de(e):e[Symbol.iterator](),r={},o("next"),o("throw"),o("return"),r[Symbol.asyncIterator]=function(){return this},r);function o(i){r[i]=e[i]&&function(a){return new Promise(function(s,p){a=e[i](a),n(s,p,a.done,a.value)})}}function n(i,a,s,p){Promise.resolve(p).then(function(c){i({value:c,done:s})},a)}}function k(e){return typeof e=="function"}function ft(e){var t=function(o){Error.call(o),o.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var zt=ft(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(o,n){return n+1+") "+o.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function qe(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,o,n,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=de(a),p=s.next();!p.done;p=s.next()){var c=p.value;c.remove(this)}}catch(A){t={error:A}}finally{try{p&&!p.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var l=this.initialTeardown;if(k(l))try{l()}catch(A){i=A instanceof zt?A.errors:[A]}var f=this._finalizers;if(f){this._finalizers=null;try{for(var u=de(f),h=u.next();!h.done;h=u.next()){var w=h.value;try{fo(w)}catch(A){i=i!=null?i:[],A instanceof zt?i=q(q([],N(i)),N(A.errors)):i.push(A)}}}catch(A){o={error:A}}finally{try{h&&!h.done&&(n=u.return)&&n.call(u)}finally{if(o)throw o.error}}}if(i)throw new zt(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)fo(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&qe(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&qe(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Tr=Fe.EMPTY;function qt(e){return e instanceof Fe||e&&"closed"in e&&k(e.remove)&&k(e.add)&&k(e.unsubscribe)}function fo(e){k(e)?e():e.unsubscribe()}var $e={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var ut={setTimeout:function(e,t){for(var r=[],o=2;o0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var o=this,n=this,i=n.hasError,a=n.isStopped,s=n.observers;return i||a?Tr:(this.currentObservers=null,s.push(r),new Fe(function(){o.currentObservers=null,qe(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var o=this,n=o.hasError,i=o.thrownError,a=o.isStopped;n?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new F;return r.source=this,r},t.create=function(r,o){return new Eo(r,o)},t}(F);var Eo=function(e){re(t,e);function t(r,o){var n=e.call(this)||this;return n.destination=r,n.source=o,n}return t.prototype.next=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.next)===null||n===void 0||n.call(o,r)},t.prototype.error=function(r){var o,n;(n=(o=this.destination)===null||o===void 0?void 0:o.error)===null||n===void 0||n.call(o,r)},t.prototype.complete=function(){var r,o;(o=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||o===void 0||o.call(r)},t.prototype._subscribe=function(r){var o,n;return(n=(o=this.source)===null||o===void 0?void 0:o.subscribe(r))!==null&&n!==void 0?n:Tr},t}(g);var _r=function(e){re(t,e);function t(r){var o=e.call(this)||this;return o._value=r,o}return Object.defineProperty(t.prototype,"value",{get:function(){return this.getValue()},enumerable:!1,configurable:!0}),t.prototype._subscribe=function(r){var o=e.prototype._subscribe.call(this,r);return!o.closed&&r.next(this._value),o},t.prototype.getValue=function(){var r=this,o=r.hasError,n=r.thrownError,i=r._value;if(o)throw n;return this._throwIfClosed(),i},t.prototype.next=function(r){e.prototype.next.call(this,this._value=r)},t}(g);var Lt={now:function(){return(Lt.delegate||Date).now()},delegate:void 0};var _t=function(e){re(t,e);function t(r,o,n){r===void 0&&(r=1/0),o===void 0&&(o=1/0),n===void 0&&(n=Lt);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=o,i._timestampProvider=n,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=o===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,o),i}return t.prototype.next=function(r){var o=this,n=o.isStopped,i=o._buffer,a=o._infiniteTimeWindow,s=o._timestampProvider,p=o._windowTime;n||(i.push(r),!a&&i.push(s.now()+p)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var o=this._innerSubscribe(r),n=this,i=n._infiniteTimeWindow,a=n._buffer,s=a.slice(),p=0;p0?e.prototype.schedule.call(this,r,o):(this.delay=o,this.state=r,this.scheduler.flush(this),this)},t.prototype.execute=function(r,o){return o>0||this.closed?e.prototype.execute.call(this,r,o):this._execute(r,o)},t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!=null&&n>0||n==null&&this.delay>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.flush(this),0)},t}(vt);var So=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t}(gt);var Hr=new So(To);var Oo=function(e){re(t,e);function t(r,o){var n=e.call(this,r,o)||this;return n.scheduler=r,n.work=o,n}return t.prototype.requestAsyncId=function(r,o,n){return n===void 0&&(n=0),n!==null&&n>0?e.prototype.requestAsyncId.call(this,r,o,n):(r.actions.push(this),r._scheduled||(r._scheduled=bt.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,o,n){var i;if(n===void 0&&(n=0),n!=null?n>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,o,n);var a=r.actions;o!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==o&&(bt.cancelAnimationFrame(o),r._scheduled=void 0)},t}(vt);var Mo=function(e){re(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var o=this._scheduled;this._scheduled=void 0;var n=this.actions,i;r=r||n.shift();do if(i=r.execute(r.state,r.delay))break;while((r=n[0])&&r.id===o&&n.shift());if(this._active=!1,i){for(;(r=n[0])&&r.id===o&&n.shift();)r.unsubscribe();throw i}},t}(gt);var me=new Mo(Oo);var M=new F(function(e){return e.complete()});function Yt(e){return e&&k(e.schedule)}function kr(e){return e[e.length-1]}function Xe(e){return k(kr(e))?e.pop():void 0}function He(e){return Yt(kr(e))?e.pop():void 0}function Bt(e,t){return typeof kr(e)=="number"?e.pop():t}var xt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function Gt(e){return k(e==null?void 0:e.then)}function Jt(e){return k(e[ht])}function Xt(e){return Symbol.asyncIterator&&k(e==null?void 0:e[Symbol.asyncIterator])}function Zt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Gi(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var er=Gi();function tr(e){return k(e==null?void 0:e[er])}function rr(e){return lo(this,arguments,function(){var r,o,n,i;return Nt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,nt(r.read())];case 3:return o=a.sent(),n=o.value,i=o.done,i?[4,nt(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,nt(n)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function or(e){return k(e==null?void 0:e.getReader)}function W(e){if(e instanceof F)return e;if(e!=null){if(Jt(e))return Ji(e);if(xt(e))return Xi(e);if(Gt(e))return Zi(e);if(Xt(e))return Lo(e);if(tr(e))return ea(e);if(or(e))return ta(e)}throw Zt(e)}function Ji(e){return new F(function(t){var r=e[ht]();if(k(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Xi(e){return new F(function(t){for(var r=0;r=2;return function(o){return o.pipe(e?b(function(n,i){return e(n,i,o)}):le,Te(1),r?Be(t):zo(function(){return new ir}))}}function Fr(e){return e<=0?function(){return M}:y(function(t,r){var o=[];t.subscribe(T(r,function(n){o.push(n),e=2,!0))}function pe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new g}:t,o=e.resetOnError,n=o===void 0?!0:o,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,p=s===void 0?!0:s;return function(c){var l,f,u,h=0,w=!1,A=!1,te=function(){f==null||f.unsubscribe(),f=void 0},ie=function(){te(),l=u=void 0,w=A=!1},J=function(){var H=l;ie(),H==null||H.unsubscribe()};return y(function(H,mt){h++,!A&&!w&&te();var ze=u=u!=null?u:r();mt.add(function(){h--,h===0&&!A&&!w&&(f=Wr(J,p))}),ze.subscribe(mt),!l&&h>0&&(l=new at({next:function(Ie){return ze.next(Ie)},error:function(Ie){A=!0,te(),f=Wr(ie,n,Ie),ze.error(Ie)},complete:function(){w=!0,te(),f=Wr(ie,a),ze.complete()}}),W(H).subscribe(l))})(c)}}function Wr(e,t){for(var r=[],o=2;oe.next(document)),e}function $(e,t=document){return Array.from(t.querySelectorAll(e))}function P(e,t=document){let r=fe(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function fe(e,t=document){return t.querySelector(e)||void 0}function Re(){var e,t,r,o;return(o=(r=(t=(e=document.activeElement)==null?void 0:e.shadowRoot)==null?void 0:t.activeElement)!=null?r:document.activeElement)!=null?o:void 0}var xa=S(d(document.body,"focusin"),d(document.body,"focusout")).pipe(_e(1),Q(void 0),m(()=>Re()||document.body),B(1));function et(e){return xa.pipe(m(t=>e.contains(t)),K())}function kt(e,t){return C(()=>S(d(e,"mouseenter").pipe(m(()=>!0)),d(e,"mouseleave").pipe(m(()=>!1))).pipe(t?Ht(r=>Me(+!r*t)):le,Q(e.matches(":hover"))))}function Bo(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)Bo(e,r)}function x(e,t,...r){let o=document.createElement(e);if(t)for(let n of Object.keys(t))typeof t[n]!="undefined"&&(typeof t[n]!="boolean"?o.setAttribute(n,t[n]):o.setAttribute(n,""));for(let n of r)Bo(o,n);return o}function sr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function wt(e){let t=x("script",{src:e});return C(()=>(document.head.appendChild(t),S(d(t,"load"),d(t,"error").pipe(v(()=>$r(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(m(()=>{}),L(()=>document.head.removeChild(t)),Te(1))))}var Go=new g,ya=C(()=>typeof ResizeObserver=="undefined"?wt("https://unpkg.com/resize-observer-polyfill"):I(void 0)).pipe(m(()=>new ResizeObserver(e=>e.forEach(t=>Go.next(t)))),v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function ce(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){let t=e;for(;t.clientWidth===0&&t.parentElement;)t=t.parentElement;return ya.pipe(E(r=>r.observe(t)),v(r=>Go.pipe(b(o=>o.target===t),L(()=>r.unobserve(t)))),m(()=>ce(e)),Q(ce(e)))}function Tt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function cr(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}function Jo(e){let t=[],r=e.parentElement;for(;r;)(e.clientWidth>r.clientWidth||e.clientHeight>r.clientHeight)&&t.push(r),r=(e=r).parentElement;return t.length===0&&t.push(document.documentElement),t}function Ue(e){return{x:e.offsetLeft,y:e.offsetTop}}function Xo(e){let t=e.getBoundingClientRect();return{x:t.x+window.scrollX,y:t.y+window.scrollY}}function Zo(e){return S(d(window,"load"),d(window,"resize")).pipe(Le(0,me),m(()=>Ue(e)),Q(Ue(e)))}function pr(e){return{x:e.scrollLeft,y:e.scrollTop}}function De(e){return S(d(e,"scroll"),d(window,"scroll"),d(window,"resize")).pipe(Le(0,me),m(()=>pr(e)),Q(pr(e)))}var en=new g,Ea=C(()=>I(new IntersectionObserver(e=>{for(let t of e)en.next(t)},{threshold:0}))).pipe(v(e=>S(Ke,I(e)).pipe(L(()=>e.disconnect()))),B(1));function tt(e){return Ea.pipe(E(t=>t.observe(e)),v(t=>en.pipe(b(({target:r})=>r===e),L(()=>t.unobserve(e)),m(({isIntersecting:r})=>r))))}function tn(e,t=16){return De(e).pipe(m(({y:r})=>{let o=ce(e),n=Tt(e);return r>=n.height-o.height-t}),K())}var lr={drawer:P("[data-md-toggle=drawer]"),search:P("[data-md-toggle=search]")};function rn(e){return lr[e].checked}function Je(e,t){lr[e].checked!==t&&lr[e].click()}function Ve(e){let t=lr[e];return d(t,"change").pipe(m(()=>t.checked),Q(t.checked))}function wa(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ta(){return S(d(window,"compositionstart").pipe(m(()=>!0)),d(window,"compositionend").pipe(m(()=>!1))).pipe(Q(!1))}function on(){let e=d(window,"keydown").pipe(b(t=>!(t.metaKey||t.ctrlKey)),m(t=>({mode:rn("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),b(({mode:t,type:r})=>{if(t==="global"){let o=Re();if(typeof o!="undefined")return!wa(o,r)}return!0}),pe());return Ta().pipe(v(t=>t?M:e))}function xe(){return new URL(location.href)}function pt(e,t=!1){if(G("navigation.instant")&&!t){let r=x("a",{href:e.href});document.body.appendChild(r),r.click(),r.remove()}else location.href=e.href}function nn(){return new g}function an(){return location.hash.slice(1)}function sn(e){let t=x("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Sa(e){return S(d(window,"hashchange"),e).pipe(m(an),Q(an()),b(t=>t.length>0),B(1))}function cn(e){return Sa(e).pipe(m(t=>fe(`[id="${t}"]`)),b(t=>typeof t!="undefined"))}function $t(e){let t=matchMedia(e);return ar(r=>t.addListener(()=>r(t.matches))).pipe(Q(t.matches))}function pn(){let e=matchMedia("print");return S(d(window,"beforeprint").pipe(m(()=>!0)),d(window,"afterprint").pipe(m(()=>!1))).pipe(Q(e.matches))}function Nr(e,t){return e.pipe(v(r=>r?t():M))}function zr(e,t){return new F(r=>{let o=new XMLHttpRequest;return o.open("GET",`${e}`),o.responseType="blob",o.addEventListener("load",()=>{o.status>=200&&o.status<300?(r.next(o.response),r.complete()):r.error(new Error(o.statusText))}),o.addEventListener("error",()=>{r.error(new Error("Network error"))}),o.addEventListener("abort",()=>{r.complete()}),typeof(t==null?void 0:t.progress$)!="undefined"&&(o.addEventListener("progress",n=>{var i;if(n.lengthComputable)t.progress$.next(n.loaded/n.total*100);else{let a=(i=o.getResponseHeader("Content-Length"))!=null?i:0;t.progress$.next(n.loaded/+a*100)}}),t.progress$.next(5)),o.send(),()=>o.abort()})}function Ne(e,t){return zr(e,t).pipe(v(r=>r.text()),m(r=>JSON.parse(r)),B(1))}function ln(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/html")),B(1))}function mn(e,t){let r=new DOMParser;return zr(e,t).pipe(v(o=>o.text()),m(o=>r.parseFromString(o,"text/xml")),B(1))}function fn(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function un(){return S(d(window,"scroll",{passive:!0}),d(window,"resize",{passive:!0})).pipe(m(fn),Q(fn()))}function dn(){return{width:innerWidth,height:innerHeight}}function hn(){return d(window,"resize",{passive:!0}).pipe(m(dn),Q(dn()))}function bn(){return z([un(),hn()]).pipe(m(([e,t])=>({offset:e,size:t})),B(1))}function mr(e,{viewport$:t,header$:r}){let o=t.pipe(Z("size")),n=z([o,r]).pipe(m(()=>Ue(e)));return z([r,t,n]).pipe(m(([{height:i},{offset:a,size:s},{x:p,y:c}])=>({offset:{x:a.x-p,y:a.y-c+i},size:s})))}function Oa(e){return d(e,"message",t=>t.data)}function Ma(e){let t=new g;return t.subscribe(r=>e.postMessage(r)),t}function vn(e,t=new Worker(e)){let r=Oa(t),o=Ma(t),n=new g;n.subscribe(o);let i=o.pipe(X(),ne(!0));return n.pipe(X(),Pe(r.pipe(U(i))),pe())}var La=P("#__config"),St=JSON.parse(La.textContent);St.base=`${new URL(St.base,xe())}`;function ye(){return St}function G(e){return St.features.includes(e)}function Ee(e,t){return typeof t!="undefined"?St.translations[e].replace("#",t.toString()):St.translations[e]}function Se(e,t=document){return P(`[data-md-component=${e}]`,t)}function ae(e,t=document){return $(`[data-md-component=${e}]`,t)}function _a(e){let t=P(".md-typeset > :first-child",e);return d(t,"click",{once:!0}).pipe(m(()=>P(".md-typeset",e)),m(r=>({hash:__md_hash(r.innerHTML)})))}function gn(e){if(!G("announce.dismiss")||!e.childElementCount)return M;if(!e.hidden){let t=P(".md-typeset",e);__md_hash(t.innerHTML)===__md_get("__announce")&&(e.hidden=!0)}return C(()=>{let t=new g;return t.subscribe(({hash:r})=>{e.hidden=!0,__md_set("__announce",r)}),_a(e).pipe(E(r=>t.next(r)),L(()=>t.complete()),m(r=>R({ref:e},r)))})}function Aa(e,{target$:t}){return t.pipe(m(r=>({hidden:r!==e})))}function xn(e,t){let r=new g;return r.subscribe(({hidden:o})=>{e.hidden=o}),Aa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))}function Pt(e,t){return t==="inline"?x("div",{class:"md-tooltip md-tooltip--inline",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"})):x("div",{class:"md-tooltip",id:e,role:"tooltip"},x("div",{class:"md-tooltip__inner md-typeset"}))}function yn(...e){return x("div",{class:"md-tooltip2",role:"tooltip"},x("div",{class:"md-tooltip2__inner md-typeset"},e))}function En(e,t){if(t=t?`${t}_annotation_${e}`:void 0,t){let r=t?`#${t}`:void 0;return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("a",{href:r,class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}else return x("aside",{class:"md-annotation",tabIndex:0},Pt(t),x("span",{class:"md-annotation__index",tabIndex:-1},x("span",{"data-md-annotation-id":e})))}function wn(e){return x("button",{class:"md-clipboard md-icon",title:Ee("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function qr(e,t){let r=t&2,o=t&1,n=Object.keys(e.terms).filter(p=>!e.terms[p]).reduce((p,c)=>[...p,x("del",null,c)," "],[]).slice(0,-1),i=ye(),a=new URL(e.location,i.base);G("search.highlight")&&a.searchParams.set("h",Object.entries(e.terms).filter(([,p])=>p).reduce((p,[c])=>`${p} ${c}`.trim(),""));let{tags:s}=ye();return x("a",{href:`${a}`,class:"md-search-result__link",tabIndex:-1},x("article",{class:"md-search-result__article md-typeset","data-md-score":e.score.toFixed(2)},r>0&&x("div",{class:"md-search-result__icon md-icon"}),r>0&&x("h1",null,e.title),r<=0&&x("h2",null,e.title),o>0&&e.text.length>0&&e.text,e.tags&&e.tags.map(p=>{let c=s?p in s?`md-tag-icon md-tag--${s[p]}`:"md-tag-icon":"";return x("span",{class:`md-tag ${c}`},p)}),o>0&&n.length>0&&x("p",{class:"md-search-result__terms"},Ee("search.result.term.missing"),": ",...n)))}function Tn(e){let t=e[0].score,r=[...e],o=ye(),n=r.findIndex(l=>!`${new URL(l.location,o.base)}`.includes("#")),[i]=r.splice(n,1),a=r.findIndex(l=>l.scoreqr(l,1)),...p.length?[x("details",{class:"md-search-result__more"},x("summary",{tabIndex:-1},x("div",null,p.length>0&&p.length===1?Ee("search.result.more.one"):Ee("search.result.more.other",p.length))),...p.map(l=>qr(l,1)))]:[]];return x("li",{class:"md-search-result__item"},c)}function Sn(e){return x("ul",{class:"md-source__facts"},Object.entries(e).map(([t,r])=>x("li",{class:`md-source__fact md-source__fact--${t}`},typeof r=="number"?sr(r):r)))}function Qr(e){let t=`tabbed-control tabbed-control--${e}`;return x("div",{class:t,hidden:!0},x("button",{class:"tabbed-button",tabIndex:-1,"aria-hidden":"true"}))}function On(e){return x("div",{class:"md-typeset__scrollwrap"},x("div",{class:"md-typeset__table"},e))}function Ca(e){var o;let t=ye(),r=new URL(`../${e.version}/`,t.base);return x("li",{class:"md-version__item"},x("a",{href:`${r}`,class:"md-version__link"},e.title,((o=t.version)==null?void 0:o.alias)&&e.aliases.length>0&&x("span",{class:"md-version__alias"},e.aliases[0])))}function Mn(e,t){var o;let r=ye();return e=e.filter(n=>{var i;return!((i=n.properties)!=null&&i.hidden)}),x("div",{class:"md-version"},x("button",{class:"md-version__current","aria-label":Ee("select.version")},t.title,((o=r.version)==null?void 0:o.alias)&&t.aliases.length>0&&x("span",{class:"md-version__alias"},t.aliases[0])),x("ul",{class:"md-version__list"},e.map(Ca)))}var Ha=0;function ka(e){let t=z([et(e),kt(e)]).pipe(m(([o,n])=>o||n),K()),r=C(()=>Jo(e)).pipe(oe(De),ct(1),m(()=>Xo(e)));return t.pipe(Ae(o=>o),v(()=>z([t,r])),m(([o,n])=>({active:o,offset:n})),pe())}function $a(e,t){let{content$:r,viewport$:o}=t,n=`__tooltip2_${Ha++}`;return C(()=>{let i=new g,a=new _r(!1);i.pipe(X(),ne(!1)).subscribe(a);let s=a.pipe(Ht(c=>Me(+!c*250,Hr)),K(),v(c=>c?r:M),E(c=>c.id=n),pe());z([i.pipe(m(({active:c})=>c)),s.pipe(v(c=>kt(c,250)),Q(!1))]).pipe(m(c=>c.some(l=>l))).subscribe(a);let p=a.pipe(b(c=>c),ee(s,o),m(([c,l,{size:f}])=>{let u=e.getBoundingClientRect(),h=u.width/2;if(l.role==="tooltip")return{x:h,y:8+u.height};if(u.y>=f.height/2){let{height:w}=ce(l);return{x:h,y:-16-w}}else return{x:h,y:16+u.height}}));return z([s,i,p]).subscribe(([c,{offset:l},f])=>{c.style.setProperty("--md-tooltip-host-x",`${l.x}px`),c.style.setProperty("--md-tooltip-host-y",`${l.y}px`),c.style.setProperty("--md-tooltip-x",`${f.x}px`),c.style.setProperty("--md-tooltip-y",`${f.y}px`),c.classList.toggle("md-tooltip2--top",f.y<0),c.classList.toggle("md-tooltip2--bottom",f.y>=0)}),a.pipe(b(c=>c),ee(s,(c,l)=>l),b(c=>c.role==="tooltip")).subscribe(c=>{let l=ce(P(":scope > *",c));c.style.setProperty("--md-tooltip-width",`${l.width}px`),c.style.setProperty("--md-tooltip-tail","0px")}),a.pipe(K(),be(me),ee(s)).subscribe(([c,l])=>{l.classList.toggle("md-tooltip2--active",c)}),z([a.pipe(b(c=>c)),s]).subscribe(([c,l])=>{l.role==="dialog"?(e.setAttribute("aria-controls",n),e.setAttribute("aria-haspopup","dialog")):e.setAttribute("aria-describedby",n)}),a.pipe(b(c=>!c)).subscribe(()=>{e.removeAttribute("aria-controls"),e.removeAttribute("aria-describedby"),e.removeAttribute("aria-haspopup")}),ka(e).pipe(E(c=>i.next(c)),L(()=>i.complete()),m(c=>R({ref:e},c)))})}function lt(e,{viewport$:t},r=document.body){return $a(e,{content$:new F(o=>{let n=e.title,i=yn(n);return o.next(i),e.removeAttribute("title"),r.append(i),()=>{i.remove(),e.setAttribute("title",n)}}),viewport$:t})}function Pa(e,t){let r=C(()=>z([Zo(e),De(t)])).pipe(m(([{x:o,y:n},i])=>{let{width:a,height:s}=ce(e);return{x:o-i.x+a/2,y:n-i.y+s/2}}));return et(e).pipe(v(o=>r.pipe(m(n=>({active:o,offset:n})),Te(+!o||1/0))))}function Ln(e,t,{target$:r}){let[o,n]=Array.from(e.children);return C(()=>{let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({offset:s}){e.style.setProperty("--md-tooltip-x",`${s.x}px`),e.style.setProperty("--md-tooltip-y",`${s.y}px`)},complete(){e.style.removeProperty("--md-tooltip-x"),e.style.removeProperty("--md-tooltip-y")}}),tt(e).pipe(U(a)).subscribe(s=>{e.toggleAttribute("data-md-visible",s)}),S(i.pipe(b(({active:s})=>s)),i.pipe(_e(250),b(({active:s})=>!s))).subscribe({next({active:s}){s?e.prepend(o):o.remove()},complete(){e.prepend(o)}}),i.pipe(Le(16,me)).subscribe(({active:s})=>{o.classList.toggle("md-tooltip--active",s)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:s})=>s)).subscribe({next(s){s?e.style.setProperty("--md-tooltip-0",`${-s}px`):e.style.removeProperty("--md-tooltip-0")},complete(){e.style.removeProperty("--md-tooltip-0")}}),d(n,"click").pipe(U(a),b(s=>!(s.metaKey||s.ctrlKey))).subscribe(s=>{s.stopPropagation(),s.preventDefault()}),d(n,"mousedown").pipe(U(a),ee(i)).subscribe(([s,{active:p}])=>{var c;if(s.button!==0||s.metaKey||s.ctrlKey)s.preventDefault();else if(p){s.preventDefault();let l=e.parentElement.closest(".md-annotation");l instanceof HTMLElement?l.focus():(c=Re())==null||c.blur()}}),r.pipe(U(a),b(s=>s===o),Ge(125)).subscribe(()=>e.focus()),Pa(e,t).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function Ra(e){return e.tagName==="CODE"?$(".c, .c1, .cm",e):[e]}function Ia(e){let t=[];for(let r of Ra(e)){let o=[],n=document.createNodeIterator(r,NodeFilter.SHOW_TEXT);for(let i=n.nextNode();i;i=n.nextNode())o.push(i);for(let i of o){let a;for(;a=/(\(\d+\))(!)?/.exec(i.textContent);){let[,s,p]=a;if(typeof p=="undefined"){let c=i.splitText(a.index);i=c.splitText(s.length),t.push(c)}else{i.textContent=s,t.push(i);break}}}}return t}function _n(e,t){t.append(...Array.from(e.childNodes))}function fr(e,t,{target$:r,print$:o}){let n=t.closest("[id]"),i=n==null?void 0:n.id,a=new Map;for(let s of Ia(t)){let[,p]=s.textContent.match(/\((\d+)\)/);fe(`:scope > li:nth-child(${p})`,e)&&(a.set(p,En(p,i)),s.replaceWith(a.get(p)))}return a.size===0?M:C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=[];for(let[l,f]of a)c.push([P(".md-typeset",f),P(`:scope > li:nth-child(${l})`,e)]);return o.pipe(U(p)).subscribe(l=>{e.hidden=!l,e.classList.toggle("md-annotation-list",l);for(let[f,u]of c)l?_n(f,u):_n(u,f)}),S(...[...a].map(([,l])=>Ln(l,t,{target$:r}))).pipe(L(()=>s.complete()),pe())})}function An(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return An(t)}}function Cn(e,t){return C(()=>{let r=An(e);return typeof r!="undefined"?fr(r,e,t):M})}var Hn=Vt(Yr());var Fa=0;function kn(e){if(e.nextElementSibling){let t=e.nextElementSibling;if(t.tagName==="OL")return t;if(t.tagName==="P"&&!t.children.length)return kn(t)}}function ja(e){return ge(e).pipe(m(({width:t})=>({scrollable:Tt(e).width>t})),Z("scrollable"))}function $n(e,t){let{matches:r}=matchMedia("(hover)"),o=C(()=>{let n=new g,i=n.pipe(Fr(1));n.subscribe(({scrollable:c})=>{c&&r?e.setAttribute("tabindex","0"):e.removeAttribute("tabindex")});let a=[];if(Hn.default.isSupported()&&(e.closest(".copy")||G("content.code.copy")&&!e.closest(".no-copy"))){let c=e.closest("pre");c.id=`__code_${Fa++}`;let l=wn(c.id);c.insertBefore(l,e),G("content.tooltips")&&a.push(lt(l,{viewport$}))}let s=e.closest(".highlight");if(s instanceof HTMLElement){let c=kn(s);if(typeof c!="undefined"&&(s.classList.contains("annotate")||G("content.code.annotate"))){let l=fr(c,e,t);a.push(ge(s).pipe(U(i),m(({width:f,height:u})=>f&&u),K(),v(f=>f?l:M)))}}return $(":scope > span[id]",e).length&&e.classList.add("md-code__content"),ja(e).pipe(E(c=>n.next(c)),L(()=>n.complete()),m(c=>R({ref:e},c)),Pe(...a))});return G("content.lazy")?tt(e).pipe(b(n=>n),Te(1),v(()=>o)):o}function Wa(e,{target$:t,print$:r}){let o=!0;return S(t.pipe(m(n=>n.closest("details:not([open])")),b(n=>e===n),m(()=>({action:"open",reveal:!0}))),r.pipe(b(n=>n||!o),E(()=>o=e.open),m(n=>({action:n?"open":"close"}))))}function Pn(e,t){return C(()=>{let r=new g;return r.subscribe(({action:o,reveal:n})=>{e.toggleAttribute("open",o==="open"),n&&e.scrollIntoView()}),Wa(e,t).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}var Rn=".node circle,.node ellipse,.node path,.node polygon,.node rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}marker{fill:var(--md-mermaid-edge-color)!important}.edgeLabel .label rect{fill:#0000}.label{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.label foreignObject{line-height:normal;overflow:visible}.label div .edgeLabel{color:var(--md-mermaid-label-fg-color)}.edgeLabel,.edgeLabel rect,.label div .edgeLabel{background-color:var(--md-mermaid-label-bg-color)}.edgeLabel,.edgeLabel rect{fill:var(--md-mermaid-label-bg-color);color:var(--md-mermaid-edge-color)}.edgePath .path,.flowchart-link{stroke:var(--md-mermaid-edge-color);stroke-width:.05rem}.edgePath .arrowheadPath{fill:var(--md-mermaid-edge-color);stroke:none}.cluster rect{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}.cluster span{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}g #flowchart-circleEnd,g #flowchart-circleStart,g #flowchart-crossEnd,g #flowchart-crossStart,g #flowchart-pointEnd,g #flowchart-pointStart{stroke:none}g.classGroup line,g.classGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.classGroup text{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.classLabel .box{fill:var(--md-mermaid-label-bg-color);background-color:var(--md-mermaid-label-bg-color);opacity:1}.classLabel .label{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.node .divider{stroke:var(--md-mermaid-node-fg-color)}.relation{stroke:var(--md-mermaid-edge-color)}.cardinality{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.cardinality text{fill:inherit!important}defs #classDiagram-compositionEnd,defs #classDiagram-compositionStart,defs #classDiagram-dependencyEnd,defs #classDiagram-dependencyStart,defs #classDiagram-extensionEnd,defs #classDiagram-extensionStart{fill:var(--md-mermaid-edge-color)!important;stroke:var(--md-mermaid-edge-color)!important}defs #classDiagram-aggregationEnd,defs #classDiagram-aggregationStart{fill:var(--md-mermaid-label-bg-color)!important;stroke:var(--md-mermaid-edge-color)!important}g.stateGroup rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}g.stateGroup .state-title{fill:var(--md-mermaid-label-fg-color)!important;font-family:var(--md-mermaid-font-family)}g.stateGroup .composit{fill:var(--md-mermaid-label-bg-color)}.nodeLabel,.nodeLabel p{color:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}a .nodeLabel{text-decoration:underline}.node circle.state-end,.node circle.state-start,.start-state{fill:var(--md-mermaid-edge-color);stroke:none}.end-state-inner,.end-state-outer{fill:var(--md-mermaid-edge-color)}.end-state-inner,.node circle.state-end{stroke:var(--md-mermaid-label-bg-color)}.transition{stroke:var(--md-mermaid-edge-color)}[id^=state-fork] rect,[id^=state-join] rect{fill:var(--md-mermaid-edge-color)!important;stroke:none!important}.statediagram-cluster.statediagram-cluster .inner{fill:var(--md-default-bg-color)}.statediagram-cluster rect{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.statediagram-state rect.divider{fill:var(--md-default-fg-color--lightest);stroke:var(--md-default-fg-color--lighter)}defs #statediagram-barbEnd{stroke:var(--md-mermaid-edge-color)}.attributeBoxEven,.attributeBoxOdd{fill:var(--md-mermaid-node-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityBox{fill:var(--md-mermaid-label-bg-color);stroke:var(--md-mermaid-node-fg-color)}.entityLabel{fill:var(--md-mermaid-label-fg-color);font-family:var(--md-mermaid-font-family)}.relationshipLabelBox{fill:var(--md-mermaid-label-bg-color);fill-opacity:1;background-color:var(--md-mermaid-label-bg-color);opacity:1}.relationshipLabel{fill:var(--md-mermaid-label-fg-color)}.relationshipLine{stroke:var(--md-mermaid-edge-color)}defs #ONE_OR_MORE_END *,defs #ONE_OR_MORE_START *,defs #ONLY_ONE_END *,defs #ONLY_ONE_START *,defs #ZERO_OR_MORE_END *,defs #ZERO_OR_MORE_START *,defs #ZERO_OR_ONE_END *,defs #ZERO_OR_ONE_START *{stroke:var(--md-mermaid-edge-color)!important}defs #ZERO_OR_MORE_END circle,defs #ZERO_OR_MORE_START circle{fill:var(--md-mermaid-label-bg-color)}.actor{fill:var(--md-mermaid-sequence-actor-bg-color);stroke:var(--md-mermaid-sequence-actor-border-color)}text.actor>tspan{fill:var(--md-mermaid-sequence-actor-fg-color);font-family:var(--md-mermaid-font-family)}line{stroke:var(--md-mermaid-sequence-actor-line-color)}.actor-man circle,.actor-man line{fill:var(--md-mermaid-sequence-actorman-bg-color);stroke:var(--md-mermaid-sequence-actorman-line-color)}.messageLine0,.messageLine1{stroke:var(--md-mermaid-sequence-message-line-color)}.note{fill:var(--md-mermaid-sequence-note-bg-color);stroke:var(--md-mermaid-sequence-note-border-color)}.loopText,.loopText>tspan,.messageText,.noteText>tspan{stroke:none;font-family:var(--md-mermaid-font-family)!important}.messageText{fill:var(--md-mermaid-sequence-message-fg-color)}.loopText,.loopText>tspan{fill:var(--md-mermaid-sequence-loop-fg-color)}.noteText>tspan{fill:var(--md-mermaid-sequence-note-fg-color)}#arrowhead path{fill:var(--md-mermaid-sequence-message-line-color);stroke:none}.loopLine{fill:var(--md-mermaid-sequence-loop-bg-color);stroke:var(--md-mermaid-sequence-loop-border-color)}.labelBox{fill:var(--md-mermaid-sequence-label-bg-color);stroke:none}.labelText,.labelText>span{fill:var(--md-mermaid-sequence-label-fg-color);font-family:var(--md-mermaid-font-family)}.sequenceNumber{fill:var(--md-mermaid-sequence-number-fg-color)}rect.rect{fill:var(--md-mermaid-sequence-box-bg-color);stroke:none}rect.rect+text.text{fill:var(--md-mermaid-sequence-box-fg-color)}defs #sequencenumber{fill:var(--md-mermaid-sequence-number-bg-color)!important}";var Br,Da=0;function Va(){return typeof mermaid=="undefined"||mermaid instanceof Element?wt("https://unpkg.com/mermaid@10/dist/mermaid.min.js"):I(void 0)}function In(e){return e.classList.remove("mermaid"),Br||(Br=Va().pipe(E(()=>mermaid.initialize({startOnLoad:!1,themeCSS:Rn,sequence:{actorFontSize:"16px",messageFontSize:"16px",noteFontSize:"16px"}})),m(()=>{}),B(1))),Br.subscribe(()=>ao(this,null,function*(){e.classList.add("mermaid");let t=`__mermaid_${Da++}`,r=x("div",{class:"mermaid"}),o=e.textContent,{svg:n,fn:i}=yield mermaid.render(t,o),a=r.attachShadow({mode:"closed"});a.innerHTML=n,e.replaceWith(r),i==null||i(a)})),Br.pipe(m(()=>({ref:e})))}var Fn=x("table");function jn(e){return e.replaceWith(Fn),Fn.replaceWith(On(e)),I({ref:e})}function Na(e){let t=e.find(r=>r.checked)||e[0];return S(...e.map(r=>d(r,"change").pipe(m(()=>P(`label[for="${r.id}"]`))))).pipe(Q(P(`label[for="${t.id}"]`)),m(r=>({active:r})))}function Wn(e,{viewport$:t,target$:r}){let o=P(".tabbed-labels",e),n=$(":scope > input",e),i=Qr("prev");e.append(i);let a=Qr("next");return e.append(a),C(()=>{let s=new g,p=s.pipe(X(),ne(!0));z([s,ge(e),tt(e)]).pipe(U(p),Le(1,me)).subscribe({next([{active:c},l]){let f=Ue(c),{width:u}=ce(c);e.style.setProperty("--md-indicator-x",`${f.x}px`),e.style.setProperty("--md-indicator-width",`${u}px`);let h=pr(o);(f.xh.x+l.width)&&o.scrollTo({left:Math.max(0,f.x-16),behavior:"smooth"})},complete(){e.style.removeProperty("--md-indicator-x"),e.style.removeProperty("--md-indicator-width")}}),z([De(o),ge(o)]).pipe(U(p)).subscribe(([c,l])=>{let f=Tt(o);i.hidden=c.x<16,a.hidden=c.x>f.width-l.width-16}),S(d(i,"click").pipe(m(()=>-1)),d(a,"click").pipe(m(()=>1))).pipe(U(p)).subscribe(c=>{let{width:l}=ce(o);o.scrollBy({left:l*c,behavior:"smooth"})}),r.pipe(U(p),b(c=>n.includes(c))).subscribe(c=>c.click()),o.classList.add("tabbed-labels--linked");for(let c of n){let l=P(`label[for="${c.id}"]`);l.replaceChildren(x("a",{href:`#${l.htmlFor}`,tabIndex:-1},...Array.from(l.childNodes))),d(l.firstElementChild,"click").pipe(U(p),b(f=>!(f.metaKey||f.ctrlKey)),E(f=>{f.preventDefault(),f.stopPropagation()})).subscribe(()=>{history.replaceState({},"",`#${l.htmlFor}`),l.click()})}return G("content.tabs.link")&&s.pipe(Ce(1),ee(t)).subscribe(([{active:c},{offset:l}])=>{let f=c.innerText.trim();if(c.hasAttribute("data-md-switching"))c.removeAttribute("data-md-switching");else{let u=e.offsetTop-l.y;for(let w of $("[data-tabs]"))for(let A of $(":scope > input",w)){let te=P(`label[for="${A.id}"]`);if(te!==c&&te.innerText.trim()===f){te.setAttribute("data-md-switching",""),A.click();break}}window.scrollTo({top:e.offsetTop-u});let h=__md_get("__tabs")||[];__md_set("__tabs",[...new Set([f,...h])])}}),s.pipe(U(p)).subscribe(()=>{for(let c of $("audio, video",e))c.pause()}),Na(n).pipe(E(c=>s.next(c)),L(()=>s.complete()),m(c=>R({ref:e},c)))}).pipe(Qe(se))}function Un(e,{viewport$:t,target$:r,print$:o}){return S(...$(".annotate:not(.highlight)",e).map(n=>Cn(n,{target$:r,print$:o})),...$("pre:not(.mermaid) > code",e).map(n=>$n(n,{target$:r,print$:o})),...$("pre.mermaid",e).map(n=>In(n)),...$("table:not([class])",e).map(n=>jn(n)),...$("details",e).map(n=>Pn(n,{target$:r,print$:o})),...$("[data-tabs]",e).map(n=>Wn(n,{viewport$:t,target$:r})),...$("[title]",e).filter(()=>G("content.tooltips")).map(n=>lt(n,{viewport$:t})))}function za(e,{alert$:t}){return t.pipe(v(r=>S(I(!0),I(!1).pipe(Ge(2e3))).pipe(m(o=>({message:r,active:o})))))}function Dn(e,t){let r=P(".md-typeset",e);return C(()=>{let o=new g;return o.subscribe(({message:n,active:i})=>{e.classList.toggle("md-dialog--active",i),r.textContent=n}),za(e,t).pipe(E(n=>o.next(n)),L(()=>o.complete()),m(n=>R({ref:e},n)))})}var qa=0;function Qa(e,t){document.body.append(e);let{width:r}=ce(e);e.style.setProperty("--md-tooltip-width",`${r}px`),e.remove();let o=cr(t),n=typeof o!="undefined"?De(o):I({x:0,y:0}),i=S(et(t),kt(t)).pipe(K());return z([i,n]).pipe(m(([a,s])=>{let{x:p,y:c}=Ue(t),l=ce(t),f=t.closest("table");return f&&t.parentElement&&(p+=f.offsetLeft+t.parentElement.offsetLeft,c+=f.offsetTop+t.parentElement.offsetTop),{active:a,offset:{x:p-s.x+l.width/2-r/2,y:c-s.y+l.height+8}}}))}function Vn(e){let t=e.title;if(!t.length)return M;let r=`__tooltip_${qa++}`,o=Pt(r,"inline"),n=P(".md-typeset",o);return n.innerHTML=t,C(()=>{let i=new g;return i.subscribe({next({offset:a}){o.style.setProperty("--md-tooltip-x",`${a.x}px`),o.style.setProperty("--md-tooltip-y",`${a.y}px`)},complete(){o.style.removeProperty("--md-tooltip-x"),o.style.removeProperty("--md-tooltip-y")}}),S(i.pipe(b(({active:a})=>a)),i.pipe(_e(250),b(({active:a})=>!a))).subscribe({next({active:a}){a?(e.insertAdjacentElement("afterend",o),e.setAttribute("aria-describedby",r),e.removeAttribute("title")):(o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t))},complete(){o.remove(),e.removeAttribute("aria-describedby"),e.setAttribute("title",t)}}),i.pipe(Le(16,me)).subscribe(({active:a})=>{o.classList.toggle("md-tooltip--active",a)}),i.pipe(ct(125,me),b(()=>!!e.offsetParent),m(()=>e.offsetParent.getBoundingClientRect()),m(({x:a})=>a)).subscribe({next(a){a?o.style.setProperty("--md-tooltip-0",`${-a}px`):o.style.removeProperty("--md-tooltip-0")},complete(){o.style.removeProperty("--md-tooltip-0")}}),Qa(o,e).pipe(E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))}).pipe(Qe(se))}function Ka({viewport$:e}){if(!G("header.autohide"))return I(!1);let t=e.pipe(m(({offset:{y:n}})=>n),Ye(2,1),m(([n,i])=>[nMath.abs(i-n.y)>100),m(([,[n]])=>n),K()),o=Ve("search");return z([e,o]).pipe(m(([{offset:n},i])=>n.y>400&&!i),K(),v(n=>n?r:I(!1)),Q(!1))}function Nn(e,t){return C(()=>z([ge(e),Ka(t)])).pipe(m(([{height:r},o])=>({height:r,hidden:o})),K((r,o)=>r.height===o.height&&r.hidden===o.hidden),B(1))}function zn(e,{header$:t,main$:r}){return C(()=>{let o=new g,n=o.pipe(X(),ne(!0));o.pipe(Z("active"),We(t)).subscribe(([{active:a},{hidden:s}])=>{e.classList.toggle("md-header--shadow",a&&!s),e.hidden=s});let i=ue($("[title]",e)).pipe(b(()=>G("content.tooltips")),oe(a=>Vn(a)));return r.subscribe(o),t.pipe(U(n),m(a=>R({ref:e},a)),Pe(i.pipe(U(n))))})}function Ya(e,{viewport$:t,header$:r}){return mr(e,{viewport$:t,header$:r}).pipe(m(({offset:{y:o}})=>{let{height:n}=ce(e);return{active:o>=n}}),Z("active"))}function qn(e,t){return C(()=>{let r=new g;r.subscribe({next({active:n}){e.classList.toggle("md-header__title--active",n)},complete(){e.classList.remove("md-header__title--active")}});let o=fe(".md-content h1");return typeof o=="undefined"?M:Ya(o,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))})}function Qn(e,{viewport$:t,header$:r}){let o=r.pipe(m(({height:i})=>i),K()),n=o.pipe(v(()=>ge(e).pipe(m(({height:i})=>({top:e.offsetTop,bottom:e.offsetTop+i})),Z("bottom"))));return z([o,n,t]).pipe(m(([i,{top:a,bottom:s},{offset:{y:p},size:{height:c}}])=>(c=Math.max(0,c-Math.max(0,a-p,i)-Math.max(0,c+p-s)),{offset:a-i,height:c,active:a-i<=p})),K((i,a)=>i.offset===a.offset&&i.height===a.height&&i.active===a.active))}function Ba(e){let t=__md_get("__palette")||{index:e.findIndex(o=>matchMedia(o.getAttribute("data-md-color-media")).matches)},r=Math.max(0,Math.min(t.index,e.length-1));return I(...e).pipe(oe(o=>d(o,"change").pipe(m(()=>o))),Q(e[r]),m(o=>({index:e.indexOf(o),color:{media:o.getAttribute("data-md-color-media"),scheme:o.getAttribute("data-md-color-scheme"),primary:o.getAttribute("data-md-color-primary"),accent:o.getAttribute("data-md-color-accent")}})),B(1))}function Kn(e){let t=$("input",e),r=x("meta",{name:"theme-color"});document.head.appendChild(r);let o=x("meta",{name:"color-scheme"});document.head.appendChild(o);let n=$t("(prefers-color-scheme: light)");return C(()=>{let i=new g;return i.subscribe(a=>{if(document.body.setAttribute("data-md-color-switching",""),a.color.media==="(prefers-color-scheme)"){let s=matchMedia("(prefers-color-scheme: light)"),p=document.querySelector(s.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");a.color.scheme=p.getAttribute("data-md-color-scheme"),a.color.primary=p.getAttribute("data-md-color-primary"),a.color.accent=p.getAttribute("data-md-color-accent")}for(let[s,p]of Object.entries(a.color))document.body.setAttribute(`data-md-color-${s}`,p);for(let s=0;sa.key==="Enter"),ee(i,(a,s)=>s)).subscribe(({index:a})=>{a=(a+1)%t.length,t[a].click(),t[a].focus()}),i.pipe(m(()=>{let a=Se("header"),s=window.getComputedStyle(a);return o.content=s.colorScheme,s.backgroundColor.match(/\d+/g).map(p=>(+p).toString(16).padStart(2,"0")).join("")})).subscribe(a=>r.content=`#${a}`),i.pipe(be(se)).subscribe(()=>{document.body.removeAttribute("data-md-color-switching")}),Ba(t).pipe(U(n.pipe(Ce(1))),st(),E(a=>i.next(a)),L(()=>i.complete()),m(a=>R({ref:e},a)))})}function Yn(e,{progress$:t}){return C(()=>{let r=new g;return r.subscribe(({value:o})=>{e.style.setProperty("--md-progress-value",`${o}`)}),t.pipe(E(o=>r.next({value:o})),L(()=>r.complete()),m(o=>({ref:e,value:o})))})}var Gr=Vt(Yr());function Ga(e){e.setAttribute("data-md-copying","");let t=e.closest("[data-copy]"),r=t?t.getAttribute("data-copy"):e.innerText;return e.removeAttribute("data-md-copying"),r.trimEnd()}function Bn({alert$:e}){Gr.default.isSupported()&&new F(t=>{new Gr.default("[data-clipboard-target], [data-clipboard-text]",{text:r=>r.getAttribute("data-clipboard-text")||Ga(P(r.getAttribute("data-clipboard-target")))}).on("success",r=>t.next(r))}).pipe(E(t=>{t.trigger.focus()}),m(()=>Ee("clipboard.copied"))).subscribe(e)}function Gn(e,t){return e.protocol=t.protocol,e.hostname=t.hostname,e}function Ja(e,t){let r=new Map;for(let o of $("url",e)){let n=P("loc",o),i=[Gn(new URL(n.textContent),t)];r.set(`${i[0]}`,i);for(let a of $("[rel=alternate]",o)){let s=a.getAttribute("href");s!=null&&i.push(Gn(new URL(s),t))}}return r}function ur(e){return mn(new URL("sitemap.xml",e)).pipe(m(t=>Ja(t,new URL(e))),ve(()=>I(new Map)))}function Xa(e,t){if(!(e.target instanceof Element))return M;let r=e.target.closest("a");if(r===null)return M;if(r.target||e.metaKey||e.ctrlKey)return M;let o=new URL(r.href);return o.search=o.hash="",t.has(`${o}`)?(e.preventDefault(),I(new URL(r.href))):M}function Jn(e){let t=new Map;for(let r of $(":scope > *",e.head))t.set(r.outerHTML,r);return t}function Xn(e){for(let t of $("[href], [src]",e))for(let r of["href","src"]){let o=t.getAttribute(r);if(o&&!/^(?:[a-z]+:)?\/\//i.test(o)){t[r]=t[r];break}}return I(e)}function Za(e){for(let o of["[data-md-component=announce]","[data-md-component=container]","[data-md-component=header-topic]","[data-md-component=outdated]","[data-md-component=logo]","[data-md-component=skip]",...G("navigation.tabs.sticky")?["[data-md-component=tabs]"]:[]]){let n=fe(o),i=fe(o,e);typeof n!="undefined"&&typeof i!="undefined"&&n.replaceWith(i)}let t=Jn(document);for(let[o,n]of Jn(e))t.has(o)?t.delete(o):document.head.appendChild(n);for(let o of t.values()){let n=o.getAttribute("name");n!=="theme-color"&&n!=="color-scheme"&&o.remove()}let r=Se("container");return je($("script",r)).pipe(v(o=>{let n=e.createElement("script");if(o.src){for(let i of o.getAttributeNames())n.setAttribute(i,o.getAttribute(i));return o.replaceWith(n),new F(i=>{n.onload=()=>i.complete()})}else return n.textContent=o.textContent,o.replaceWith(n),M}),X(),ne(document))}function Zn({location$:e,viewport$:t,progress$:r}){let o=ye();if(location.protocol==="file:")return M;let n=ur(o.base);I(document).subscribe(Xn);let i=d(document.body,"click").pipe(We(n),v(([p,c])=>Xa(p,c)),pe()),a=d(window,"popstate").pipe(m(xe),pe());i.pipe(ee(t)).subscribe(([p,{offset:c}])=>{history.replaceState(c,""),history.pushState(null,"",p)}),S(i,a).subscribe(e);let s=e.pipe(Z("pathname"),v(p=>ln(p,{progress$:r}).pipe(ve(()=>(pt(p,!0),M)))),v(Xn),v(Za),pe());return S(s.pipe(ee(e,(p,c)=>c)),s.pipe(v(()=>e),Z("pathname"),v(()=>e),Z("hash")),e.pipe(K((p,c)=>p.pathname===c.pathname&&p.hash===c.hash),v(()=>i),E(()=>history.back()))).subscribe(p=>{var c,l;history.state!==null||!p.hash?window.scrollTo(0,(l=(c=history.state)==null?void 0:c.y)!=null?l:0):(history.scrollRestoration="auto",sn(p.hash),history.scrollRestoration="manual")}),e.subscribe(()=>{history.scrollRestoration="manual"}),d(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"}),t.pipe(Z("offset"),_e(100)).subscribe(({offset:p})=>{history.replaceState(p,"")}),s}var ri=Vt(ti());function oi(e){let t=e.separator.split("|").map(n=>n.replace(/(\(\?[!=<][^)]+\))/g,"").length===0?"\uFFFD":n).join("|"),r=new RegExp(t,"img"),o=(n,i,a)=>`${i}${a}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();let i=new RegExp(`(^|${e.separator}|)(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(r,"|")})`,"img");return a=>(0,ri.default)(a).replace(i,o).replace(/<\/mark>(\s+)]*>/img,"$1")}}function It(e){return e.type===1}function dr(e){return e.type===3}function ni(e,t){let r=vn(e);return S(I(location.protocol!=="file:"),Ve("search")).pipe(Ae(o=>o),v(()=>t)).subscribe(({config:o,docs:n})=>r.next({type:0,data:{config:o,docs:n,options:{suggest:G("search.suggest")}}})),r}function ii({document$:e}){let t=ye(),r=Ne(new URL("../versions.json",t.base)).pipe(ve(()=>M)),o=r.pipe(m(n=>{let[,i]=t.base.match(/([^/]+)\/?$/);return n.find(({version:a,aliases:s})=>a===i||s.includes(i))||n[0]}));r.pipe(m(n=>new Map(n.map(i=>[`${new URL(`../${i.version}/`,t.base)}`,i]))),v(n=>d(document.body,"click").pipe(b(i=>!i.metaKey&&!i.ctrlKey),ee(o),v(([i,a])=>{if(i.target instanceof Element){let s=i.target.closest("a");if(s&&!s.target&&n.has(s.href)){let p=s.href;return!i.target.closest(".md-version")&&n.get(p)===a?M:(i.preventDefault(),I(p))}}return M}),v(i=>ur(new URL(i)).pipe(m(a=>{let p=xe().href.replace(t.base,i);return a.has(p.split("#")[0])?new URL(p):new URL(i)})))))).subscribe(n=>pt(n,!0)),z([r,o]).subscribe(([n,i])=>{P(".md-header__topic").appendChild(Mn(n,i))}),e.pipe(v(()=>o)).subscribe(n=>{var a;let i=__md_get("__outdated",sessionStorage);if(i===null){i=!0;let s=((a=t.version)==null?void 0:a.default)||"latest";Array.isArray(s)||(s=[s]);e:for(let p of s)for(let c of n.aliases.concat(n.version))if(new RegExp(p,"i").test(c)){i=!1;break e}__md_set("__outdated",i,sessionStorage)}if(i)for(let s of ae("outdated"))s.hidden=!1})}function ns(e,{worker$:t}){let{searchParams:r}=xe();r.has("q")&&(Je("search",!0),e.value=r.get("q"),e.focus(),Ve("search").pipe(Ae(i=>!i)).subscribe(()=>{let i=xe();i.searchParams.delete("q"),history.replaceState({},"",`${i}`)}));let o=et(e),n=S(t.pipe(Ae(It)),d(e,"keyup"),o).pipe(m(()=>e.value),K());return z([n,o]).pipe(m(([i,a])=>({value:i,focus:a})),B(1))}function ai(e,{worker$:t}){let r=new g,o=r.pipe(X(),ne(!0));z([t.pipe(Ae(It)),r],(i,a)=>a).pipe(Z("value")).subscribe(({value:i})=>t.next({type:2,data:i})),r.pipe(Z("focus")).subscribe(({focus:i})=>{i&&Je("search",i)}),d(e.form,"reset").pipe(U(o)).subscribe(()=>e.focus());let n=P("header [for=__search]");return d(n,"click").subscribe(()=>e.focus()),ns(e,{worker$:t}).pipe(E(i=>r.next(i)),L(()=>r.complete()),m(i=>R({ref:e},i)),B(1))}function si(e,{worker$:t,query$:r}){let o=new g,n=tn(e.parentElement).pipe(b(Boolean)),i=e.parentElement,a=P(":scope > :first-child",e),s=P(":scope > :last-child",e);Ve("search").subscribe(l=>s.setAttribute("role",l?"list":"presentation")),o.pipe(ee(r),Ur(t.pipe(Ae(It)))).subscribe(([{items:l},{value:f}])=>{switch(l.length){case 0:a.textContent=f.length?Ee("search.result.none"):Ee("search.result.placeholder");break;case 1:a.textContent=Ee("search.result.one");break;default:let u=sr(l.length);a.textContent=Ee("search.result.other",u)}});let p=o.pipe(E(()=>s.innerHTML=""),v(({items:l})=>S(I(...l.slice(0,10)),I(...l.slice(10)).pipe(Ye(4),Vr(n),v(([f])=>f)))),m(Tn),pe());return p.subscribe(l=>s.appendChild(l)),p.pipe(oe(l=>{let f=fe("details",l);return typeof f=="undefined"?M:d(f,"toggle").pipe(U(o),m(()=>f))})).subscribe(l=>{l.open===!1&&l.offsetTop<=i.scrollTop&&i.scrollTo({top:l.offsetTop})}),t.pipe(b(dr),m(({data:l})=>l)).pipe(E(l=>o.next(l)),L(()=>o.complete()),m(l=>R({ref:e},l)))}function is(e,{query$:t}){return t.pipe(m(({value:r})=>{let o=xe();return o.hash="",r=r.replace(/\s+/g,"+").replace(/&/g,"%26").replace(/=/g,"%3D"),o.search=`q=${r}`,{url:o}}))}function ci(e,t){let r=new g,o=r.pipe(X(),ne(!0));return r.subscribe(({url:n})=>{e.setAttribute("data-clipboard-text",e.href),e.href=`${n}`}),d(e,"click").pipe(U(o)).subscribe(n=>n.preventDefault()),is(e,t).pipe(E(n=>r.next(n)),L(()=>r.complete()),m(n=>R({ref:e},n)))}function pi(e,{worker$:t,keyboard$:r}){let o=new g,n=Se("search-query"),i=S(d(n,"keydown"),d(n,"focus")).pipe(be(se),m(()=>n.value),K());return o.pipe(We(i),m(([{suggest:s},p])=>{let c=p.split(/([\s-]+)/);if(s!=null&&s.length&&c[c.length-1]){let l=s[s.length-1];l.startsWith(c[c.length-1])&&(c[c.length-1]=l)}else c.length=0;return c})).subscribe(s=>e.innerHTML=s.join("").replace(/\s/g," ")),r.pipe(b(({mode:s})=>s==="search")).subscribe(s=>{switch(s.type){case"ArrowRight":e.innerText.length&&n.selectionStart===n.value.length&&(n.value=e.innerText);break}}),t.pipe(b(dr),m(({data:s})=>s)).pipe(E(s=>o.next(s)),L(()=>o.complete()),m(()=>({ref:e})))}function li(e,{index$:t,keyboard$:r}){let o=ye();try{let n=ni(o.search,t),i=Se("search-query",e),a=Se("search-result",e);d(e,"click").pipe(b(({target:p})=>p instanceof Element&&!!p.closest("a"))).subscribe(()=>Je("search",!1)),r.pipe(b(({mode:p})=>p==="search")).subscribe(p=>{let c=Re();switch(p.type){case"Enter":if(c===i){let l=new Map;for(let f of $(":first-child [href]",a)){let u=f.firstElementChild;l.set(f,parseFloat(u.getAttribute("data-md-score")))}if(l.size){let[[f]]=[...l].sort(([,u],[,h])=>h-u);f.click()}p.claim()}break;case"Escape":case"Tab":Je("search",!1),i.blur();break;case"ArrowUp":case"ArrowDown":if(typeof c=="undefined")i.focus();else{let l=[i,...$(":not(details) > [href], summary, details[open] [href]",a)],f=Math.max(0,(Math.max(0,l.indexOf(c))+l.length+(p.type==="ArrowUp"?-1:1))%l.length);l[f].focus()}p.claim();break;default:i!==Re()&&i.focus()}}),r.pipe(b(({mode:p})=>p==="global")).subscribe(p=>{switch(p.type){case"f":case"s":case"/":i.focus(),i.select(),p.claim();break}});let s=ai(i,{worker$:n});return S(s,si(a,{worker$:n,query$:s})).pipe(Pe(...ae("search-share",e).map(p=>ci(p,{query$:s})),...ae("search-suggest",e).map(p=>pi(p,{worker$:n,keyboard$:r}))))}catch(n){return e.hidden=!0,Ke}}function mi(e,{index$:t,location$:r}){return z([t,r.pipe(Q(xe()),b(o=>!!o.searchParams.get("h")))]).pipe(m(([o,n])=>oi(o.config)(n.searchParams.get("h"))),m(o=>{var a;let n=new Map,i=document.createNodeIterator(e,NodeFilter.SHOW_TEXT);for(let s=i.nextNode();s;s=i.nextNode())if((a=s.parentElement)!=null&&a.offsetHeight){let p=s.textContent,c=o(p);c.length>p.length&&n.set(s,c)}for(let[s,p]of n){let{childNodes:c}=x("span",null,p);s.replaceWith(...Array.from(c))}return{ref:e,nodes:n}}))}function as(e,{viewport$:t,main$:r}){let o=e.closest(".md-grid"),n=o.offsetTop-o.parentElement.offsetTop;return z([r,t]).pipe(m(([{offset:i,height:a},{offset:{y:s}}])=>(a=a+Math.min(n,Math.max(0,s-i))-n,{height:a,locked:s>=i+n})),K((i,a)=>i.height===a.height&&i.locked===a.locked))}function Jr(e,o){var n=o,{header$:t}=n,r=io(n,["header$"]);let i=P(".md-sidebar__scrollwrap",e),{y:a}=Ue(i);return C(()=>{let s=new g,p=s.pipe(X(),ne(!0)),c=s.pipe(Le(0,me));return c.pipe(ee(t)).subscribe({next([{height:l},{height:f}]){i.style.height=`${l-2*a}px`,e.style.top=`${f}px`},complete(){i.style.height="",e.style.top=""}}),c.pipe(Ae()).subscribe(()=>{for(let l of $(".md-nav__link--active[href]",e)){if(!l.clientHeight)continue;let f=l.closest(".md-sidebar__scrollwrap");if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2})}}}),ue($("label[tabindex]",e)).pipe(oe(l=>d(l,"click").pipe(be(se),m(()=>l),U(p)))).subscribe(l=>{let f=P(`[id="${l.htmlFor}"]`);P(`[aria-labelledby="${l.id}"]`).setAttribute("aria-expanded",`${f.checked}`)}),as(e,r).pipe(E(l=>s.next(l)),L(()=>s.complete()),m(l=>R({ref:e},l)))})}function fi(e,t){if(typeof t!="undefined"){let r=`https://api.github.com/repos/${e}/${t}`;return Ct(Ne(`${r}/releases/latest`).pipe(ve(()=>M),m(o=>({version:o.tag_name})),Be({})),Ne(r).pipe(ve(()=>M),m(o=>({stars:o.stargazers_count,forks:o.forks_count})),Be({}))).pipe(m(([o,n])=>R(R({},o),n)))}else{let r=`https://api.github.com/users/${e}`;return Ne(r).pipe(m(o=>({repositories:o.public_repos})),Be({}))}}function ui(e,t){let r=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Ne(r).pipe(ve(()=>M),m(({star_count:o,forks_count:n})=>({stars:o,forks:n})),Be({}))}function di(e){let t=e.match(/^.+github\.com\/([^/]+)\/?([^/]+)?/i);if(t){let[,r,o]=t;return fi(r,o)}if(t=e.match(/^.+?([^/]*gitlab[^/]+)\/(.+?)\/?$/i),t){let[,r,o]=t;return ui(r,o)}return M}var ss;function cs(e){return ss||(ss=C(()=>{let t=__md_get("__source",sessionStorage);if(t)return I(t);if(ae("consent").length){let o=__md_get("__consent");if(!(o&&o.github))return M}return di(e.href).pipe(E(o=>__md_set("__source",o,sessionStorage)))}).pipe(ve(()=>M),b(t=>Object.keys(t).length>0),m(t=>({facts:t})),B(1)))}function hi(e){let t=P(":scope > :last-child",e);return C(()=>{let r=new g;return r.subscribe(({facts:o})=>{t.appendChild(Sn(o)),t.classList.add("md-source__repository--active")}),cs(e).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ps(e,{viewport$:t,header$:r}){return ge(document.body).pipe(v(()=>mr(e,{header$:r,viewport$:t})),m(({offset:{y:o}})=>({hidden:o>=10})),Z("hidden"))}function bi(e,t){return C(()=>{let r=new g;return r.subscribe({next({hidden:o}){e.hidden=o},complete(){e.hidden=!1}}),(G("navigation.tabs.sticky")?I({hidden:!1}):ps(e,t)).pipe(E(o=>r.next(o)),L(()=>r.complete()),m(o=>R({ref:e},o)))})}function ls(e,{viewport$:t,header$:r}){let o=new Map,n=$(".md-nav__link",e);for(let s of n){let p=decodeURIComponent(s.hash.substring(1)),c=fe(`[id="${p}"]`);typeof c!="undefined"&&o.set(s,c)}let i=r.pipe(Z("height"),m(({height:s})=>{let p=Se("main"),c=P(":scope > :first-child",p);return s+.8*(c.offsetTop-p.offsetTop)}),pe());return ge(document.body).pipe(Z("height"),v(s=>C(()=>{let p=[];return I([...o].reduce((c,[l,f])=>{for(;p.length&&o.get(p[p.length-1]).tagName>=f.tagName;)p.pop();let u=f.offsetTop;for(;!u&&f.parentElement;)f=f.parentElement,u=f.offsetTop;let h=f.offsetParent;for(;h;h=h.offsetParent)u+=h.offsetTop;return c.set([...p=[...p,l]].reverse(),u)},new Map))}).pipe(m(p=>new Map([...p].sort(([,c],[,l])=>c-l))),We(i),v(([p,c])=>t.pipe(jr(([l,f],{offset:{y:u},size:h})=>{let w=u+h.height>=Math.floor(s.height);for(;f.length;){let[,A]=f[0];if(A-c=u&&!w)f=[l.pop(),...f];else break}return[l,f]},[[],[...p]]),K((l,f)=>l[0]===f[0]&&l[1]===f[1])))))).pipe(m(([s,p])=>({prev:s.map(([c])=>c),next:p.map(([c])=>c)})),Q({prev:[],next:[]}),Ye(2,1),m(([s,p])=>s.prev.length{let i=new g,a=i.pipe(X(),ne(!0));if(i.subscribe(({prev:s,next:p})=>{for(let[c]of p)c.classList.remove("md-nav__link--passed"),c.classList.remove("md-nav__link--active");for(let[c,[l]]of s.entries())l.classList.add("md-nav__link--passed"),l.classList.toggle("md-nav__link--active",c===s.length-1)}),G("toc.follow")){let s=S(t.pipe(_e(1),m(()=>{})),t.pipe(_e(250),m(()=>"smooth")));i.pipe(b(({prev:p})=>p.length>0),We(o.pipe(be(se))),ee(s)).subscribe(([[{prev:p}],c])=>{let[l]=p[p.length-1];if(l.offsetHeight){let f=cr(l);if(typeof f!="undefined"){let u=l.offsetTop-f.offsetTop,{height:h}=ce(f);f.scrollTo({top:u-h/2,behavior:c})}}})}return G("navigation.tracking")&&t.pipe(U(a),Z("offset"),_e(250),Ce(1),U(n.pipe(Ce(1))),st({delay:250}),ee(i)).subscribe(([,{prev:s}])=>{let p=xe(),c=s[s.length-1];if(c&&c.length){let[l]=c,{hash:f}=new URL(l.href);p.hash!==f&&(p.hash=f,history.replaceState({},"",`${p}`))}else p.hash="",history.replaceState({},"",`${p}`)}),ls(e,{viewport$:t,header$:r}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))})}function ms(e,{viewport$:t,main$:r,target$:o}){let n=t.pipe(m(({offset:{y:a}})=>a),Ye(2,1),m(([a,s])=>a>s&&s>0),K()),i=r.pipe(m(({active:a})=>a));return z([i,n]).pipe(m(([a,s])=>!(a&&s)),K(),U(o.pipe(Ce(1))),ne(!0),st({delay:250}),m(a=>({hidden:a})))}function gi(e,{viewport$:t,header$:r,main$:o,target$:n}){let i=new g,a=i.pipe(X(),ne(!0));return i.subscribe({next({hidden:s}){e.hidden=s,s?(e.setAttribute("tabindex","-1"),e.blur()):e.removeAttribute("tabindex")},complete(){e.style.top="",e.hidden=!0,e.removeAttribute("tabindex")}}),r.pipe(U(a),Z("height")).subscribe(({height:s})=>{e.style.top=`${s+16}px`}),d(e,"click").subscribe(s=>{s.preventDefault(),window.scrollTo({top:0})}),ms(e,{viewport$:t,main$:o,target$:n}).pipe(E(s=>i.next(s)),L(()=>i.complete()),m(s=>R({ref:e},s)))}function xi({document$:e,viewport$:t}){e.pipe(v(()=>$(".md-ellipsis")),oe(r=>tt(r).pipe(U(e.pipe(Ce(1))),b(o=>o),m(()=>r),Te(1))),b(r=>r.offsetWidth{let o=r.innerText,n=r.closest("a")||r;return n.title=o,lt(n,{viewport$:t}).pipe(U(e.pipe(Ce(1))),L(()=>n.removeAttribute("title")))})).subscribe(),e.pipe(v(()=>$(".md-status")),oe(r=>lt(r,{viewport$:t}))).subscribe()}function yi({document$:e,tablet$:t}){e.pipe(v(()=>$(".md-toggle--indeterminate")),E(r=>{r.indeterminate=!0,r.checked=!1}),oe(r=>d(r,"change").pipe(Dr(()=>r.classList.contains("md-toggle--indeterminate")),m(()=>r))),ee(t)).subscribe(([r,o])=>{r.classList.remove("md-toggle--indeterminate"),o&&(r.checked=!1)})}function fs(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}function Ei({document$:e}){e.pipe(v(()=>$("[data-md-scrollfix]")),E(t=>t.removeAttribute("data-md-scrollfix")),b(fs),oe(t=>d(t,"touchstart").pipe(m(()=>t)))).subscribe(t=>{let r=t.scrollTop;r===0?t.scrollTop=1:r+t.offsetHeight===t.scrollHeight&&(t.scrollTop=r-1)})}function wi({viewport$:e,tablet$:t}){z([Ve("search"),t]).pipe(m(([r,o])=>r&&!o),v(r=>I(r).pipe(Ge(r?400:100))),ee(e)).subscribe(([r,{offset:{y:o}}])=>{if(r)document.body.setAttribute("data-md-scrolllock",""),document.body.style.top=`-${o}px`;else{let n=-1*parseInt(document.body.style.top,10);document.body.removeAttribute("data-md-scrolllock"),document.body.style.top="",n&&window.scrollTo(0,n)}})}Object.entries||(Object.entries=function(e){let t=[];for(let r of Object.keys(e))t.push([r,e[r]]);return t});Object.values||(Object.values=function(e){let t=[];for(let r of Object.keys(e))t.push(e[r]);return t});typeof Element!="undefined"&&(Element.prototype.scrollTo||(Element.prototype.scrollTo=function(e,t){typeof e=="object"?(this.scrollLeft=e.left,this.scrollTop=e.top):(this.scrollLeft=e,this.scrollTop=t)}),Element.prototype.replaceWith||(Element.prototype.replaceWith=function(...e){let t=this.parentNode;if(t){e.length===0&&t.removeChild(this);for(let r=e.length-1;r>=0;r--){let o=e[r];typeof o=="string"?o=document.createTextNode(o):o.parentNode&&o.parentNode.removeChild(o),r?t.insertBefore(this.previousSibling,o):t.replaceChild(o,this)}}}));function us(){return location.protocol==="file:"?wt(`${new URL("search/search_index.js",Xr.base)}`).pipe(m(()=>__index),B(1)):Ne(new URL("search/search_index.json",Xr.base))}document.documentElement.classList.remove("no-js");document.documentElement.classList.add("js");var ot=Yo(),jt=nn(),Ot=cn(jt),Zr=on(),Oe=bn(),hr=$t("(min-width: 960px)"),Si=$t("(min-width: 1220px)"),Oi=pn(),Xr=ye(),Mi=document.forms.namedItem("search")?us():Ke,eo=new g;Bn({alert$:eo});var to=new g;G("navigation.instant")&&Zn({location$:jt,viewport$:Oe,progress$:to}).subscribe(ot);var Ti;((Ti=Xr.version)==null?void 0:Ti.provider)==="mike"&&ii({document$:ot});S(jt,Ot).pipe(Ge(125)).subscribe(()=>{Je("drawer",!1),Je("search",!1)});Zr.pipe(b(({mode:e})=>e==="global")).subscribe(e=>{switch(e.type){case"p":case",":let t=fe("link[rel=prev]");typeof t!="undefined"&&pt(t);break;case"n":case".":let r=fe("link[rel=next]");typeof r!="undefined"&&pt(r);break;case"Enter":let o=Re();o instanceof HTMLLabelElement&&o.click()}});xi({viewport$:Oe,document$:ot});yi({document$:ot,tablet$:hr});Ei({document$:ot});wi({viewport$:Oe,tablet$:hr});var rt=Nn(Se("header"),{viewport$:Oe}),Ft=ot.pipe(m(()=>Se("main")),v(e=>Qn(e,{viewport$:Oe,header$:rt})),B(1)),ds=S(...ae("consent").map(e=>xn(e,{target$:Ot})),...ae("dialog").map(e=>Dn(e,{alert$:eo})),...ae("header").map(e=>zn(e,{viewport$:Oe,header$:rt,main$:Ft})),...ae("palette").map(e=>Kn(e)),...ae("progress").map(e=>Yn(e,{progress$:to})),...ae("search").map(e=>li(e,{index$:Mi,keyboard$:Zr})),...ae("source").map(e=>hi(e))),hs=C(()=>S(...ae("announce").map(e=>gn(e)),...ae("content").map(e=>Un(e,{viewport$:Oe,target$:Ot,print$:Oi})),...ae("content").map(e=>G("search.highlight")?mi(e,{index$:Mi,location$:jt}):M),...ae("header-title").map(e=>qn(e,{viewport$:Oe,header$:rt})),...ae("sidebar").map(e=>e.getAttribute("data-md-type")==="navigation"?Nr(Si,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft})):Nr(hr,()=>Jr(e,{viewport$:Oe,header$:rt,main$:Ft}))),...ae("tabs").map(e=>bi(e,{viewport$:Oe,header$:rt})),...ae("toc").map(e=>vi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})),...ae("top").map(e=>gi(e,{viewport$:Oe,header$:rt,main$:Ft,target$:Ot})))),Li=ot.pipe(v(()=>hs),Pe(ds),B(1));Li.subscribe();window.document$=ot;window.location$=jt;window.target$=Ot;window.keyboard$=Zr;window.viewport$=Oe;window.tablet$=hr;window.screen$=Si;window.print$=Oi;window.alert$=eo;window.progress$=to;window.component$=Li;})(); +//# sourceMappingURL=bundle.ad660dcc.min.js.map + diff --git a/assets/javascripts/bundle.ad660dcc.min.js.map b/assets/javascripts/bundle.ad660dcc.min.js.map new file mode 100644 index 000000000..6d61170f1 --- /dev/null +++ b/assets/javascripts/bundle.ad660dcc.min.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "sources": ["node_modules/focus-visible/dist/focus-visible.js", "node_modules/clipboard/dist/clipboard.js", "node_modules/escape-html/index.js", "src/templates/assets/javascripts/bundle.ts", "node_modules/rxjs/node_modules/tslib/tslib.es6.js", "node_modules/rxjs/src/internal/util/isFunction.ts", "node_modules/rxjs/src/internal/util/createErrorClass.ts", "node_modules/rxjs/src/internal/util/UnsubscriptionError.ts", "node_modules/rxjs/src/internal/util/arrRemove.ts", "node_modules/rxjs/src/internal/Subscription.ts", "node_modules/rxjs/src/internal/config.ts", "node_modules/rxjs/src/internal/scheduler/timeoutProvider.ts", "node_modules/rxjs/src/internal/util/reportUnhandledError.ts", "node_modules/rxjs/src/internal/util/noop.ts", "node_modules/rxjs/src/internal/NotificationFactories.ts", "node_modules/rxjs/src/internal/util/errorContext.ts", "node_modules/rxjs/src/internal/Subscriber.ts", "node_modules/rxjs/src/internal/symbol/observable.ts", "node_modules/rxjs/src/internal/util/identity.ts", "node_modules/rxjs/src/internal/util/pipe.ts", "node_modules/rxjs/src/internal/Observable.ts", "node_modules/rxjs/src/internal/util/lift.ts", "node_modules/rxjs/src/internal/operators/OperatorSubscriber.ts", "node_modules/rxjs/src/internal/scheduler/animationFrameProvider.ts", "node_modules/rxjs/src/internal/util/ObjectUnsubscribedError.ts", "node_modules/rxjs/src/internal/Subject.ts", "node_modules/rxjs/src/internal/BehaviorSubject.ts", "node_modules/rxjs/src/internal/scheduler/dateTimestampProvider.ts", "node_modules/rxjs/src/internal/ReplaySubject.ts", "node_modules/rxjs/src/internal/scheduler/Action.ts", "node_modules/rxjs/src/internal/scheduler/intervalProvider.ts", "node_modules/rxjs/src/internal/scheduler/AsyncAction.ts", "node_modules/rxjs/src/internal/Scheduler.ts", "node_modules/rxjs/src/internal/scheduler/AsyncScheduler.ts", "node_modules/rxjs/src/internal/scheduler/async.ts", "node_modules/rxjs/src/internal/scheduler/QueueAction.ts", "node_modules/rxjs/src/internal/scheduler/QueueScheduler.ts", "node_modules/rxjs/src/internal/scheduler/queue.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameAction.ts", "node_modules/rxjs/src/internal/scheduler/AnimationFrameScheduler.ts", "node_modules/rxjs/src/internal/scheduler/animationFrame.ts", "node_modules/rxjs/src/internal/observable/empty.ts", "node_modules/rxjs/src/internal/util/isScheduler.ts", "node_modules/rxjs/src/internal/util/args.ts", "node_modules/rxjs/src/internal/util/isArrayLike.ts", "node_modules/rxjs/src/internal/util/isPromise.ts", "node_modules/rxjs/src/internal/util/isInteropObservable.ts", "node_modules/rxjs/src/internal/util/isAsyncIterable.ts", "node_modules/rxjs/src/internal/util/throwUnobservableError.ts", "node_modules/rxjs/src/internal/symbol/iterator.ts", "node_modules/rxjs/src/internal/util/isIterable.ts", "node_modules/rxjs/src/internal/util/isReadableStreamLike.ts", "node_modules/rxjs/src/internal/observable/innerFrom.ts", "node_modules/rxjs/src/internal/util/executeSchedule.ts", "node_modules/rxjs/src/internal/operators/observeOn.ts", "node_modules/rxjs/src/internal/operators/subscribeOn.ts", "node_modules/rxjs/src/internal/scheduled/scheduleObservable.ts", "node_modules/rxjs/src/internal/scheduled/schedulePromise.ts", "node_modules/rxjs/src/internal/scheduled/scheduleArray.ts", "node_modules/rxjs/src/internal/scheduled/scheduleIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleAsyncIterable.ts", "node_modules/rxjs/src/internal/scheduled/scheduleReadableStreamLike.ts", "node_modules/rxjs/src/internal/scheduled/scheduled.ts", "node_modules/rxjs/src/internal/observable/from.ts", "node_modules/rxjs/src/internal/observable/of.ts", "node_modules/rxjs/src/internal/observable/throwError.ts", "node_modules/rxjs/src/internal/util/EmptyError.ts", "node_modules/rxjs/src/internal/util/isDate.ts", "node_modules/rxjs/src/internal/operators/map.ts", "node_modules/rxjs/src/internal/util/mapOneOrManyArgs.ts", "node_modules/rxjs/src/internal/util/argsArgArrayOrObject.ts", "node_modules/rxjs/src/internal/util/createObject.ts", "node_modules/rxjs/src/internal/observable/combineLatest.ts", "node_modules/rxjs/src/internal/operators/mergeInternals.ts", "node_modules/rxjs/src/internal/operators/mergeMap.ts", "node_modules/rxjs/src/internal/operators/mergeAll.ts", "node_modules/rxjs/src/internal/operators/concatAll.ts", "node_modules/rxjs/src/internal/observable/concat.ts", "node_modules/rxjs/src/internal/observable/defer.ts", "node_modules/rxjs/src/internal/observable/fromEvent.ts", "node_modules/rxjs/src/internal/observable/fromEventPattern.ts", "node_modules/rxjs/src/internal/observable/timer.ts", "node_modules/rxjs/src/internal/observable/merge.ts", "node_modules/rxjs/src/internal/observable/never.ts", "node_modules/rxjs/src/internal/util/argsOrArgArray.ts", "node_modules/rxjs/src/internal/operators/filter.ts", "node_modules/rxjs/src/internal/observable/zip.ts", "node_modules/rxjs/src/internal/operators/audit.ts", "node_modules/rxjs/src/internal/operators/auditTime.ts", "node_modules/rxjs/src/internal/operators/bufferCount.ts", "node_modules/rxjs/src/internal/operators/catchError.ts", "node_modules/rxjs/src/internal/operators/scanInternals.ts", "node_modules/rxjs/src/internal/operators/combineLatest.ts", "node_modules/rxjs/src/internal/operators/combineLatestWith.ts", "node_modules/rxjs/src/internal/operators/debounce.ts", "node_modules/rxjs/src/internal/operators/debounceTime.ts", "node_modules/rxjs/src/internal/operators/defaultIfEmpty.ts", "node_modules/rxjs/src/internal/operators/take.ts", "node_modules/rxjs/src/internal/operators/ignoreElements.ts", "node_modules/rxjs/src/internal/operators/mapTo.ts", "node_modules/rxjs/src/internal/operators/delayWhen.ts", "node_modules/rxjs/src/internal/operators/delay.ts", "node_modules/rxjs/src/internal/operators/distinctUntilChanged.ts", "node_modules/rxjs/src/internal/operators/distinctUntilKeyChanged.ts", "node_modules/rxjs/src/internal/operators/throwIfEmpty.ts", "node_modules/rxjs/src/internal/operators/endWith.ts", "node_modules/rxjs/src/internal/operators/finalize.ts", "node_modules/rxjs/src/internal/operators/first.ts", "node_modules/rxjs/src/internal/operators/takeLast.ts", "node_modules/rxjs/src/internal/operators/merge.ts", "node_modules/rxjs/src/internal/operators/mergeWith.ts", "node_modules/rxjs/src/internal/operators/repeat.ts", "node_modules/rxjs/src/internal/operators/scan.ts", "node_modules/rxjs/src/internal/operators/share.ts", "node_modules/rxjs/src/internal/operators/shareReplay.ts", "node_modules/rxjs/src/internal/operators/skip.ts", "node_modules/rxjs/src/internal/operators/skipUntil.ts", "node_modules/rxjs/src/internal/operators/startWith.ts", "node_modules/rxjs/src/internal/operators/switchMap.ts", "node_modules/rxjs/src/internal/operators/takeUntil.ts", "node_modules/rxjs/src/internal/operators/takeWhile.ts", "node_modules/rxjs/src/internal/operators/tap.ts", "node_modules/rxjs/src/internal/operators/throttle.ts", "node_modules/rxjs/src/internal/operators/throttleTime.ts", "node_modules/rxjs/src/internal/operators/withLatestFrom.ts", "node_modules/rxjs/src/internal/operators/zip.ts", "node_modules/rxjs/src/internal/operators/zipWith.ts", "src/templates/assets/javascripts/browser/document/index.ts", "src/templates/assets/javascripts/browser/element/_/index.ts", "src/templates/assets/javascripts/browser/element/focus/index.ts", "src/templates/assets/javascripts/browser/element/hover/index.ts", "src/templates/assets/javascripts/utilities/h/index.ts", "src/templates/assets/javascripts/utilities/round/index.ts", "src/templates/assets/javascripts/browser/script/index.ts", "src/templates/assets/javascripts/browser/element/size/_/index.ts", "src/templates/assets/javascripts/browser/element/size/content/index.ts", "src/templates/assets/javascripts/browser/element/offset/_/index.ts", "src/templates/assets/javascripts/browser/element/offset/content/index.ts", "src/templates/assets/javascripts/browser/element/visibility/index.ts", "src/templates/assets/javascripts/browser/toggle/index.ts", "src/templates/assets/javascripts/browser/keyboard/index.ts", "src/templates/assets/javascripts/browser/location/_/index.ts", "src/templates/assets/javascripts/browser/location/hash/index.ts", "src/templates/assets/javascripts/browser/media/index.ts", "src/templates/assets/javascripts/browser/request/index.ts", "src/templates/assets/javascripts/browser/viewport/offset/index.ts", "src/templates/assets/javascripts/browser/viewport/size/index.ts", "src/templates/assets/javascripts/browser/viewport/_/index.ts", "src/templates/assets/javascripts/browser/viewport/at/index.ts", "src/templates/assets/javascripts/browser/worker/index.ts", "src/templates/assets/javascripts/_/index.ts", "src/templates/assets/javascripts/components/_/index.ts", "src/templates/assets/javascripts/components/announce/index.ts", "src/templates/assets/javascripts/components/consent/index.ts", "src/templates/assets/javascripts/templates/tooltip/index.tsx", "src/templates/assets/javascripts/templates/annotation/index.tsx", "src/templates/assets/javascripts/templates/clipboard/index.tsx", "src/templates/assets/javascripts/templates/search/index.tsx", "src/templates/assets/javascripts/templates/source/index.tsx", "src/templates/assets/javascripts/templates/tabbed/index.tsx", "src/templates/assets/javascripts/templates/table/index.tsx", "src/templates/assets/javascripts/templates/version/index.tsx", "src/templates/assets/javascripts/components/tooltip2/index.ts", "src/templates/assets/javascripts/components/content/annotation/_/index.ts", "src/templates/assets/javascripts/components/content/annotation/list/index.ts", "src/templates/assets/javascripts/components/content/annotation/block/index.ts", "src/templates/assets/javascripts/components/content/code/_/index.ts", "src/templates/assets/javascripts/components/content/details/index.ts", "src/templates/assets/javascripts/components/content/mermaid/index.css", "src/templates/assets/javascripts/components/content/mermaid/index.ts", "src/templates/assets/javascripts/components/content/table/index.ts", "src/templates/assets/javascripts/components/content/tabs/index.ts", "src/templates/assets/javascripts/components/content/_/index.ts", "src/templates/assets/javascripts/components/dialog/index.ts", "src/templates/assets/javascripts/components/tooltip/index.ts", "src/templates/assets/javascripts/components/header/_/index.ts", "src/templates/assets/javascripts/components/header/title/index.ts", "src/templates/assets/javascripts/components/main/index.ts", "src/templates/assets/javascripts/components/palette/index.ts", "src/templates/assets/javascripts/components/progress/index.ts", "src/templates/assets/javascripts/integrations/clipboard/index.ts", "src/templates/assets/javascripts/integrations/sitemap/index.ts", "src/templates/assets/javascripts/integrations/instant/index.ts", "src/templates/assets/javascripts/integrations/search/highlighter/index.ts", "src/templates/assets/javascripts/integrations/search/worker/message/index.ts", "src/templates/assets/javascripts/integrations/search/worker/_/index.ts", "src/templates/assets/javascripts/integrations/version/index.ts", "src/templates/assets/javascripts/components/search/query/index.ts", "src/templates/assets/javascripts/components/search/result/index.ts", "src/templates/assets/javascripts/components/search/share/index.ts", "src/templates/assets/javascripts/components/search/suggest/index.ts", "src/templates/assets/javascripts/components/search/_/index.ts", "src/templates/assets/javascripts/components/search/highlight/index.ts", "src/templates/assets/javascripts/components/sidebar/index.ts", "src/templates/assets/javascripts/components/source/facts/github/index.ts", "src/templates/assets/javascripts/components/source/facts/gitlab/index.ts", "src/templates/assets/javascripts/components/source/facts/_/index.ts", "src/templates/assets/javascripts/components/source/_/index.ts", "src/templates/assets/javascripts/components/tabs/index.ts", "src/templates/assets/javascripts/components/toc/index.ts", "src/templates/assets/javascripts/components/top/index.ts", "src/templates/assets/javascripts/patches/ellipsis/index.ts", "src/templates/assets/javascripts/patches/indeterminate/index.ts", "src/templates/assets/javascripts/patches/scrollfix/index.ts", "src/templates/assets/javascripts/patches/scrolllock/index.ts", "src/templates/assets/javascripts/polyfills/index.ts"], + "sourcesContent": ["(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesAllowlist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesAllowlist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. \u00AF\\_(\u30C4)_/\u00AF\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n", "/*!\n * clipboard.js v2.0.11\n * https://clipboardjs.com/\n *\n * Licensed MIT \u00A9 Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function() { // webpackBootstrap\n/******/ \tvar __webpack_modules__ = ({\n\n/***/ 686:\n/***/ (function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n\n// EXPORTS\n__webpack_require__.d(__webpack_exports__, {\n \"default\": function() { return /* binding */ clipboard; }\n});\n\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(279);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(370);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(817);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n;// CONCATENATED MODULE: ./src/common/command.js\n/**\n * Executes a given operation type.\n * @param {String} type\n * @return {Boolean}\n */\nfunction command(type) {\n try {\n return document.execCommand(type);\n } catch (err) {\n return false;\n }\n}\n;// CONCATENATED MODULE: ./src/actions/cut.js\n\n\n/**\n * Cut action wrapper.\n * @param {String|HTMLElement} target\n * @return {String}\n */\n\nvar ClipboardActionCut = function ClipboardActionCut(target) {\n var selectedText = select_default()(target);\n command('cut');\n return selectedText;\n};\n\n/* harmony default export */ var actions_cut = (ClipboardActionCut);\n;// CONCATENATED MODULE: ./src/common/create-fake-element.js\n/**\n * Creates a fake textarea element with a value.\n * @param {String} value\n * @return {HTMLElement}\n */\nfunction createFakeElement(value) {\n var isRTL = document.documentElement.getAttribute('dir') === 'rtl';\n var fakeElement = document.createElement('textarea'); // Prevent zooming on iOS\n\n fakeElement.style.fontSize = '12pt'; // Reset box model\n\n fakeElement.style.border = '0';\n fakeElement.style.padding = '0';\n fakeElement.style.margin = '0'; // Move element out of screen horizontally\n\n fakeElement.style.position = 'absolute';\n fakeElement.style[isRTL ? 'right' : 'left'] = '-9999px'; // Move element to the same position vertically\n\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n fakeElement.style.top = \"\".concat(yPosition, \"px\");\n fakeElement.setAttribute('readonly', '');\n fakeElement.value = value;\n return fakeElement;\n}\n;// CONCATENATED MODULE: ./src/actions/copy.js\n\n\n\n/**\n * Create fake copy action wrapper using a fake element.\n * @param {String} target\n * @param {Object} options\n * @return {String}\n */\n\nvar fakeCopyAction = function fakeCopyAction(value, options) {\n var fakeElement = createFakeElement(value);\n options.container.appendChild(fakeElement);\n var selectedText = select_default()(fakeElement);\n command('copy');\n fakeElement.remove();\n return selectedText;\n};\n/**\n * Copy action wrapper.\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @return {String}\n */\n\n\nvar ClipboardActionCopy = function ClipboardActionCopy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n var selectedText = '';\n\n if (typeof target === 'string') {\n selectedText = fakeCopyAction(target, options);\n } else if (target instanceof HTMLInputElement && !['text', 'search', 'url', 'tel', 'password'].includes(target === null || target === void 0 ? void 0 : target.type)) {\n // If input type doesn't support `setSelectionRange`. Simulate it. https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange\n selectedText = fakeCopyAction(target.value, options);\n } else {\n selectedText = select_default()(target);\n command('copy');\n }\n\n return selectedText;\n};\n\n/* harmony default export */ var actions_copy = (ClipboardActionCopy);\n;// CONCATENATED MODULE: ./src/actions/default.js\nfunction _typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return _typeof(obj); }\n\n\n\n/**\n * Inner function which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n * @param {Object} options\n */\n\nvar ClipboardActionDefault = function ClipboardActionDefault() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n // Defines base properties passed from constructor.\n var _options$action = options.action,\n action = _options$action === void 0 ? 'copy' : _options$action,\n container = options.container,\n target = options.target,\n text = options.text; // Sets the `action` to be performed which can be either 'copy' or 'cut'.\n\n if (action !== 'copy' && action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n } // Sets the `target` property using an element that will be have its content copied.\n\n\n if (target !== undefined) {\n if (target && _typeof(target) === 'object' && target.nodeType === 1) {\n if (action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n } // Define selection strategy based on `text` property.\n\n\n if (text) {\n return actions_copy(text, {\n container: container\n });\n } // Defines which selection strategy based on `target` property.\n\n\n if (target) {\n return action === 'cut' ? actions_cut(target) : actions_copy(target, {\n container: container\n });\n }\n};\n\n/* harmony default export */ var actions_default = (ClipboardActionDefault);\n;// CONCATENATED MODULE: ./src/clipboard.js\nfunction clipboard_typeof(obj) { \"@babel/helpers - typeof\"; if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") { clipboard_typeof = function _typeof(obj) { return typeof obj; }; } else { clipboard_typeof = function _typeof(obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; }; } return clipboard_typeof(obj); }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }\n\nfunction _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function\"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); }\n\nfunction _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }\n\nfunction _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }\n\nfunction _possibleConstructorReturn(self, call) { if (call && (clipboard_typeof(call) === \"object\" || typeof call === \"function\")) { return call; } return _assertThisInitialized(self); }\n\nfunction _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return self; }\n\nfunction _isNativeReflectConstruct() { if (typeof Reflect === \"undefined\" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === \"function\") return true; try { Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); return true; } catch (e) { return false; } }\n\nfunction _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }\n\n\n\n\n\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\nfunction getAttributeValue(suffix, element) {\n var attribute = \"data-clipboard-\".concat(suffix);\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\n\nvar Clipboard = /*#__PURE__*/function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n var _super = _createSuper(Clipboard);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n var _this;\n\n _classCallCheck(this, Clipboard);\n\n _this = _super.call(this);\n\n _this.resolveOptions(options);\n\n _this.listenClick(trigger);\n\n return _this;\n }\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n _createClass(Clipboard, [{\n key: \"resolveOptions\",\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: \"listenClick\",\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: \"onClick\",\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n var action = this.action(trigger) || 'copy';\n var text = actions_default({\n action: action,\n container: this.container,\n target: this.target(trigger),\n text: this.text(trigger)\n }); // Fires an event based on the copy operation result.\n\n this.emit(text ? 'success' : 'error', {\n action: action,\n text: text,\n trigger: trigger,\n clearSelection: function clearSelection() {\n if (trigger) {\n trigger.focus();\n }\n\n window.getSelection().removeAllRanges();\n }\n });\n }\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultAction\",\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: \"defaultTarget\",\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n /**\n * Allow fire programmatically a copy action\n * @param {String|HTMLElement} target\n * @param {Object} options\n * @returns Text copied.\n */\n\n }, {\n key: \"defaultText\",\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: \"destroy\",\n value: function destroy() {\n this.listener.destroy();\n }\n }], [{\n key: \"copy\",\n value: function copy(target) {\n var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {\n container: document.body\n };\n return actions_copy(target, options);\n }\n /**\n * Allow fire programmatically a cut action\n * @param {String|HTMLElement} target\n * @returns Text cutted.\n */\n\n }, {\n key: \"cut\",\n value: function cut(target) {\n return actions_cut(target);\n }\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: \"isSupported\",\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n return support;\n }\n }]);\n\n return Clipboard;\n}((tiny_emitter_default()));\n\n/* harmony default export */ var clipboard = (Clipboard);\n\n/***/ }),\n\n/***/ 828:\n/***/ (function(module) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n\n/***/ 438:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar closest = __webpack_require__(828);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n\n/***/ 879:\n/***/ (function(__unused_webpack_module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n\n/***/ 370:\n/***/ (function(module, __unused_webpack_exports, __webpack_require__) {\n\nvar is = __webpack_require__(879);\nvar delegate = __webpack_require__(438);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n\n/***/ 817:\n/***/ (function(module) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n\n/***/ 279:\n/***/ (function(module) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ })\n\n/******/ \t});\n/************************************************************************/\n/******/ \t// The module cache\n/******/ \tvar __webpack_module_cache__ = {};\n/******/ \t\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(__webpack_module_cache__[moduleId]) {\n/******/ \t\t\treturn __webpack_module_cache__[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = __webpack_module_cache__[moduleId] = {\n/******/ \t\t\t// no module.id needed\n/******/ \t\t\t// no module.loaded needed\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/ \t\n/******/ \t\t// Execute the module function\n/******/ \t\t__webpack_modules__[moduleId](module, module.exports, __webpack_require__);\n/******/ \t\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/ \t\n/************************************************************************/\n/******/ \t/* webpack/runtime/compat get default export */\n/******/ \t!function() {\n/******/ \t\t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t\t__webpack_require__.n = function(module) {\n/******/ \t\t\tvar getter = module && module.__esModule ?\n/******/ \t\t\t\tfunction() { return module['default']; } :\n/******/ \t\t\t\tfunction() { return module; };\n/******/ \t\t\t__webpack_require__.d(getter, { a: getter });\n/******/ \t\t\treturn getter;\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/define property getters */\n/******/ \t!function() {\n/******/ \t\t// define getter functions for harmony exports\n/******/ \t\t__webpack_require__.d = function(exports, definition) {\n/******/ \t\t\tfor(var key in definition) {\n/******/ \t\t\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n/******/ \t\t\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n/******/ \t\t\t\t}\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t}();\n/******/ \t\n/******/ \t/* webpack/runtime/hasOwnProperty shorthand */\n/******/ \t!function() {\n/******/ \t\t__webpack_require__.o = function(obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }\n/******/ \t}();\n/******/ \t\n/************************************************************************/\n/******/ \t// module exports must be returned from runtime so entry inlining is disabled\n/******/ \t// startup\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(686);\n/******/ })()\n.default;\n});", "/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n", "/*\n * Copyright (c) 2016-2024 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"focus-visible\"\n\nimport {\n EMPTY,\n NEVER,\n Observable,\n Subject,\n defer,\n delay,\n filter,\n map,\n merge,\n mergeWith,\n shareReplay,\n switchMap\n} from \"rxjs\"\n\nimport { configuration, feature } from \"./_\"\nimport {\n at,\n getActiveElement,\n getOptionalElement,\n requestJSON,\n setLocation,\n setToggle,\n watchDocument,\n watchKeyboard,\n watchLocation,\n watchLocationTarget,\n watchMedia,\n watchPrint,\n watchScript,\n watchViewport\n} from \"./browser\"\nimport {\n getComponentElement,\n getComponentElements,\n mountAnnounce,\n mountBackToTop,\n mountConsent,\n mountContent,\n mountDialog,\n mountHeader,\n mountHeaderTitle,\n mountPalette,\n mountProgress,\n mountSearch,\n mountSearchHiglight,\n mountSidebar,\n mountSource,\n mountTableOfContents,\n mountTabs,\n watchHeader,\n watchMain\n} from \"./components\"\nimport {\n SearchIndex,\n setupClipboardJS,\n setupInstantNavigation,\n setupVersionSelector\n} from \"./integrations\"\nimport {\n patchEllipsis,\n patchIndeterminate,\n patchScrollfix,\n patchScrolllock\n} from \"./patches\"\nimport \"./polyfills\"\n\n/* ----------------------------------------------------------------------------\n * Functions - @todo refactor\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch search index\n *\n * @returns Search index observable\n */\nfunction fetchSearchIndex(): Observable {\n if (location.protocol === \"file:\") {\n return watchScript(\n `${new URL(\"search/search_index.js\", config.base)}`\n )\n .pipe(\n // @ts-ignore - @todo fix typings\n map(() => __index),\n shareReplay(1)\n )\n } else {\n return requestJSON(\n new URL(\"search/search_index.json\", config.base)\n )\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Application\n * ------------------------------------------------------------------------- */\n\n/* Yay, JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Set up navigation observables and subjects */\nconst document$ = watchDocument()\nconst location$ = watchLocation()\nconst target$ = watchLocationTarget(location$)\nconst keyboard$ = watchKeyboard()\n\n/* Set up media observables */\nconst viewport$ = watchViewport()\nconst tablet$ = watchMedia(\"(min-width: 960px)\")\nconst screen$ = watchMedia(\"(min-width: 1220px)\")\nconst print$ = watchPrint()\n\n/* Retrieve search index, if search is enabled */\nconst config = configuration()\nconst index$ = document.forms.namedItem(\"search\")\n ? fetchSearchIndex()\n : NEVER\n\n/* Set up Clipboard.js integration */\nconst alert$ = new Subject()\nsetupClipboardJS({ alert$ })\n\n/* Set up progress indicator */\nconst progress$ = new Subject()\n\n/* Set up instant navigation, if enabled */\nif (feature(\"navigation.instant\"))\n setupInstantNavigation({ location$, viewport$, progress$ })\n .subscribe(document$)\n\n/* Set up version selector */\nif (config.version?.provider === \"mike\")\n setupVersionSelector({ document$ })\n\n/* Always close drawer and search on navigation */\nmerge(location$, target$)\n .pipe(\n delay(125)\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n setToggle(\"search\", false)\n })\n\n/* Set up global keyboard handlers */\nkeyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\")\n )\n .subscribe(key => {\n switch (key.type) {\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getOptionalElement(\"link[rel=prev]\")\n if (typeof prev !== \"undefined\")\n setLocation(prev)\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getOptionalElement(\"link[rel=next]\")\n if (typeof next !== \"undefined\")\n setLocation(next)\n break\n\n /* Expand navigation, see https://bit.ly/3ZjG5io */\n case \"Enter\":\n const active = getActiveElement()\n if (active instanceof HTMLLabelElement)\n active.click()\n }\n })\n\n/* Set up patches */\npatchEllipsis({ viewport$, document$ })\npatchIndeterminate({ document$, tablet$ })\npatchScrollfix({ document$ })\npatchScrolllock({ viewport$, tablet$ })\n\n/* Set up header and main area observable */\nconst header$ = watchHeader(getComponentElement(\"header\"), { viewport$ })\nconst main$ = document$\n .pipe(\n map(() => getComponentElement(\"main\")),\n switchMap(el => watchMain(el, { viewport$, header$ })),\n shareReplay(1)\n )\n\n/* Set up control component observables */\nconst control$ = merge(\n\n /* Consent */\n ...getComponentElements(\"consent\")\n .map(el => mountConsent(el, { target$ })),\n\n /* Dialog */\n ...getComponentElements(\"dialog\")\n .map(el => mountDialog(el, { alert$ })),\n\n /* Header */\n ...getComponentElements(\"header\")\n .map(el => mountHeader(el, { viewport$, header$, main$ })),\n\n /* Color palette */\n ...getComponentElements(\"palette\")\n .map(el => mountPalette(el)),\n\n /* Progress bar */\n ...getComponentElements(\"progress\")\n .map(el => mountProgress(el, { progress$ })),\n\n /* Search */\n ...getComponentElements(\"search\")\n .map(el => mountSearch(el, { index$, keyboard$ })),\n\n /* Repository information */\n ...getComponentElements(\"source\")\n .map(el => mountSource(el))\n)\n\n/* Set up content component observables */\nconst content$ = defer(() => merge(\n\n /* Announcement bar */\n ...getComponentElements(\"announce\")\n .map(el => mountAnnounce(el)),\n\n /* Content */\n ...getComponentElements(\"content\")\n .map(el => mountContent(el, { viewport$, target$, print$ })),\n\n /* Search highlighting */\n ...getComponentElements(\"content\")\n .map(el => feature(\"search.highlight\")\n ? mountSearchHiglight(el, { index$, location$ })\n : EMPTY\n ),\n\n /* Header title */\n ...getComponentElements(\"header-title\")\n .map(el => mountHeaderTitle(el, { viewport$, header$ })),\n\n /* Sidebar */\n ...getComponentElements(\"sidebar\")\n .map(el => el.getAttribute(\"data-md-type\") === \"navigation\"\n ? at(screen$, () => mountSidebar(el, { viewport$, header$, main$ }))\n : at(tablet$, () => mountSidebar(el, { viewport$, header$, main$ }))\n ),\n\n /* Navigation tabs */\n ...getComponentElements(\"tabs\")\n .map(el => mountTabs(el, { viewport$, header$ })),\n\n /* Table of contents */\n ...getComponentElements(\"toc\")\n .map(el => mountTableOfContents(el, {\n viewport$, header$, main$, target$\n })),\n\n /* Back-to-top button */\n ...getComponentElements(\"top\")\n .map(el => mountBackToTop(el, { viewport$, header$, main$, target$ }))\n))\n\n/* Set up component observables */\nconst component$ = document$\n .pipe(\n switchMap(() => content$),\n mergeWith(control$),\n shareReplay(1)\n )\n\n/* Subscribe to all components */\ncomponent$.subscribe()\n\n/* ----------------------------------------------------------------------------\n * Exports\n * ------------------------------------------------------------------------- */\n\nwindow.document$ = document$ /* Document observable */\nwindow.location$ = location$ /* Location subject */\nwindow.target$ = target$ /* Location target observable */\nwindow.keyboard$ = keyboard$ /* Keyboard observable */\nwindow.viewport$ = viewport$ /* Viewport observable */\nwindow.tablet$ = tablet$ /* Media tablet observable */\nwindow.screen$ = screen$ /* Media screen observable */\nwindow.print$ = print$ /* Media print observable */\nwindow.alert$ = alert$ /* Alert subject */\nwindow.progress$ = progress$ /* Progress indicator subject */\nwindow.component$ = component$ /* Component observable */\n", "/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n if (typeof b !== \"function\" && b !== null)\r\n throw new TypeError(\"Class extends value \" + String(b) + \" is not a constructor or null\");\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\n/** @deprecated */\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n}\r\n\r\nexport function __spreadArray(to, from, pack) {\r\n if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {\r\n if (ar || !(i in from)) {\r\n if (!ar) ar = Array.prototype.slice.call(from, 0, i);\r\n ar[i] = from[i];\r\n }\r\n }\r\n return to.concat(ar || Array.prototype.slice.call(from));\r\n}\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, state, kind, f) {\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a getter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot read private member from an object whose class did not declare it\");\r\n return kind === \"m\" ? f : kind === \"a\" ? f.call(receiver) : f ? f.value : state.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, state, value, kind, f) {\r\n if (kind === \"m\") throw new TypeError(\"Private method is not writable\");\r\n if (kind === \"a\" && !f) throw new TypeError(\"Private accessor was defined without a setter\");\r\n if (typeof state === \"function\" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError(\"Cannot write private member to an object whose class did not declare it\");\r\n return (kind === \"a\" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;\r\n}\r\n", "/**\n * Returns true if the object is a function.\n * @param value The value to check\n */\nexport function isFunction(value: any): value is (...args: any[]) => any {\n return typeof value === 'function';\n}\n", "/**\n * Used to create Error subclasses until the community moves away from ES5.\n *\n * This is because compiling from TypeScript down to ES5 has issues with subclassing Errors\n * as well as other built-in types: https://github.com/Microsoft/TypeScript/issues/12123\n *\n * @param createImpl A factory function to create the actual constructor implementation. The returned\n * function should be a named function that calls `_super` internally.\n */\nexport function createErrorClass(createImpl: (_super: any) => any): T {\n const _super = (instance: any) => {\n Error.call(instance);\n instance.stack = new Error().stack;\n };\n\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface UnsubscriptionError extends Error {\n readonly errors: any[];\n}\n\nexport interface UnsubscriptionErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (errors: any[]): UnsubscriptionError;\n}\n\n/**\n * An error thrown when one or more errors have occurred during the\n * `unsubscribe` of a {@link Subscription}.\n */\nexport const UnsubscriptionError: UnsubscriptionErrorCtor = createErrorClass(\n (_super) =>\n function UnsubscriptionErrorImpl(this: any, errors: (Error | string)[]) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n }\n);\n", "/**\n * Removes an item from an array, mutating it.\n * @param arr The array to remove the item from\n * @param item The item to remove\n */\nexport function arrRemove(arr: T[] | undefined | null, item: T) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { SubscriptionLike, TeardownLogic, Unsubscribable } from './types';\nimport { arrRemove } from './util/arrRemove';\n\n/**\n * Represents a disposable resource, such as the execution of an Observable. A\n * Subscription has one important method, `unsubscribe`, that takes no argument\n * and just disposes the resource held by the subscription.\n *\n * Additionally, subscriptions may be grouped together through the `add()`\n * method, which will attach a child Subscription to the current Subscription.\n * When a Subscription is unsubscribed, all its children (and its grandchildren)\n * will be unsubscribed as well.\n *\n * @class Subscription\n */\nexport class Subscription implements SubscriptionLike {\n /** @nocollapse */\n public static EMPTY = (() => {\n const empty = new Subscription();\n empty.closed = true;\n return empty;\n })();\n\n /**\n * A flag to indicate whether this Subscription has already been unsubscribed.\n */\n public closed = false;\n\n private _parentage: Subscription[] | Subscription | null = null;\n\n /**\n * The list of registered finalizers to execute upon unsubscription. Adding and removing from this\n * list occurs in the {@link #add} and {@link #remove} methods.\n */\n private _finalizers: Exclude[] | null = null;\n\n /**\n * @param initialTeardown A function executed first as part of the finalization\n * process that is kicked off when {@link #unsubscribe} is called.\n */\n constructor(private initialTeardown?: () => void) {}\n\n /**\n * Disposes the resources held by the subscription. May, for instance, cancel\n * an ongoing Observable execution or cancel any other type of work that\n * started when the Subscription was created.\n * @return {void}\n */\n unsubscribe(): void {\n let errors: any[] | undefined;\n\n if (!this.closed) {\n this.closed = true;\n\n // Remove this from it's parents.\n const { _parentage } = this;\n if (_parentage) {\n this._parentage = null;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n } else {\n _parentage.remove(this);\n }\n }\n\n const { initialTeardown: initialFinalizer } = this;\n if (isFunction(initialFinalizer)) {\n try {\n initialFinalizer();\n } catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n\n const { _finalizers } = this;\n if (_finalizers) {\n this._finalizers = null;\n for (const finalizer of _finalizers) {\n try {\n execFinalizer(finalizer);\n } catch (err) {\n errors = errors ?? [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n } else {\n errors.push(err);\n }\n }\n }\n }\n\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n\n /**\n * Adds a finalizer to this subscription, so that finalization will be unsubscribed/called\n * when this subscription is unsubscribed. If this subscription is already {@link #closed},\n * because it has already been unsubscribed, then whatever finalizer is passed to it\n * will automatically be executed (unless the finalizer itself is also a closed subscription).\n *\n * Closed Subscriptions cannot be added as finalizers to any subscription. Adding a closed\n * subscription to a any subscription will result in no operation. (A noop).\n *\n * Adding a subscription to itself, or adding `null` or `undefined` will not perform any\n * operation at all. (A noop).\n *\n * `Subscription` instances that are added to this instance will automatically remove themselves\n * if they are unsubscribed. Functions and {@link Unsubscribable} objects that you wish to remove\n * will need to be removed manually with {@link #remove}\n *\n * @param teardown The finalization logic to add to this subscription.\n */\n add(teardown: TeardownLogic): void {\n // Only add the finalizer if it's not undefined\n // and don't add a subscription to itself.\n if (teardown && teardown !== this) {\n if (this.closed) {\n // If this subscription is already closed,\n // execute whatever finalizer is handed to it automatically.\n execFinalizer(teardown);\n } else {\n if (teardown instanceof Subscription) {\n // We don't add closed subscriptions, and we don't add the same subscription\n // twice. Subscription unsubscribe is idempotent.\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._finalizers = this._finalizers ?? []).push(teardown);\n }\n }\n }\n\n /**\n * Checks to see if a this subscription already has a particular parent.\n * This will signal that this subscription has already been added to the parent in question.\n * @param parent the parent to check for\n */\n private _hasParent(parent: Subscription) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n\n /**\n * Adds a parent to this subscription so it can be removed from the parent if it\n * unsubscribes on it's own.\n *\n * NOTE: THIS ASSUMES THAT {@link _hasParent} HAS ALREADY BEEN CHECKED.\n * @param parent The parent subscription to add\n */\n private _addParent(parent: Subscription) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n\n /**\n * Called on a child when it is removed via {@link #remove}.\n * @param parent The parent to remove\n */\n private _removeParent(parent: Subscription) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n } else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n\n /**\n * Removes a finalizer from this subscription that was previously added with the {@link #add} method.\n *\n * Note that `Subscription` instances, when unsubscribed, will automatically remove themselves\n * from every other `Subscription` they have been added to. This means that using the `remove` method\n * is not a common thing and should be used thoughtfully.\n *\n * If you add the same finalizer instance of a function or an unsubscribable object to a `Subscription` instance\n * more than once, you will need to call `remove` the same number of times to remove all instances.\n *\n * All finalizer instances are removed to free up memory upon unsubscription.\n *\n * @param teardown The finalizer to remove from this subscription\n */\n remove(teardown: Exclude): void {\n const { _finalizers } = this;\n _finalizers && arrRemove(_finalizers, teardown);\n\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\n\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\n\nexport function isSubscription(value: any): value is Subscription {\n return (\n value instanceof Subscription ||\n (value && 'closed' in value && isFunction(value.remove) && isFunction(value.add) && isFunction(value.unsubscribe))\n );\n}\n\nfunction execFinalizer(finalizer: Unsubscribable | (() => void)) {\n if (isFunction(finalizer)) {\n finalizer();\n } else {\n finalizer.unsubscribe();\n }\n}\n", "import { Subscriber } from './Subscriber';\nimport { ObservableNotification } from './types';\n\n/**\n * The {@link GlobalConfig} object for RxJS. It is used to configure things\n * like how to react on unhandled errors.\n */\nexport const config: GlobalConfig = {\n onUnhandledError: null,\n onStoppedNotification: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n\n/**\n * The global configuration object for RxJS, used to configure things\n * like how to react on unhandled errors. Accessible via {@link config}\n * object.\n */\nexport interface GlobalConfig {\n /**\n * A registration point for unhandled errors from RxJS. These are errors that\n * cannot were not handled by consuming code in the usual subscription path. For\n * example, if you have this configured, and you subscribe to an observable without\n * providing an error handler, errors from that subscription will end up here. This\n * will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onUnhandledError: ((err: any) => void) | null;\n\n /**\n * A registration point for notifications that cannot be sent to subscribers because they\n * have completed, errored or have been explicitly unsubscribed. By default, next, complete\n * and error notifications sent to stopped subscribers are noops. However, sometimes callers\n * might want a different behavior. For example, with sources that attempt to report errors\n * to stopped subscribers, a caller can configure RxJS to throw an unhandled error instead.\n * This will _always_ be called asynchronously on another job in the runtime. This is because\n * we do not want errors thrown in this user-configured handler to interfere with the\n * behavior of the library.\n */\n onStoppedNotification: ((notification: ObservableNotification, subscriber: Subscriber) => void) | null;\n\n /**\n * The promise constructor used by default for {@link Observable#toPromise toPromise} and {@link Observable#forEach forEach}\n * methods.\n *\n * @deprecated As of version 8, RxJS will no longer support this sort of injection of a\n * Promise constructor. If you need a Promise implementation other than native promises,\n * please polyfill/patch Promise as you see appropriate. Will be removed in v8.\n */\n Promise?: PromiseConstructorLike;\n\n /**\n * If true, turns on synchronous error rethrowing, which is a deprecated behavior\n * in v6 and higher. This behavior enables bad patterns like wrapping a subscribe\n * call in a try/catch block. It also enables producer interference, a nasty bug\n * where a multicast can be broken for all observers by a downstream consumer with\n * an unhandled error. DO NOT USE THIS FLAG UNLESS IT'S NEEDED TO BUY TIME\n * FOR MIGRATION REASONS.\n *\n * @deprecated As of version 8, RxJS will no longer support synchronous throwing\n * of unhandled errors. All errors will be thrown on a separate call stack to prevent bad\n * behaviors described above. Will be removed in v8.\n */\n useDeprecatedSynchronousErrorHandling: boolean;\n\n /**\n * If true, enables an as-of-yet undocumented feature from v5: The ability to access\n * `unsubscribe()` via `this` context in `next` functions created in observers passed\n * to `subscribe`.\n *\n * This is being removed because the performance was severely problematic, and it could also cause\n * issues when types other than POJOs are passed to subscribe as subscribers, as they will likely have\n * their `this` context overwritten.\n *\n * @deprecated As of version 8, RxJS will no longer support altering the\n * context of next functions provided as part of an observer to Subscribe. Instead,\n * you will have access to a subscription or a signal or token that will allow you to do things like\n * unsubscribe and test closed status. Will be removed in v8.\n */\n useDeprecatedNextContext: boolean;\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetTimeoutFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearTimeoutFunction = (handle: TimerHandle) => void;\n\ninterface TimeoutProvider {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n delegate:\n | {\n setTimeout: SetTimeoutFunction;\n clearTimeout: ClearTimeoutFunction;\n }\n | undefined;\n}\n\nexport const timeoutProvider: TimeoutProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setTimeout(handler: () => void, timeout?: number, ...args) {\n const { delegate } = timeoutProvider;\n if (delegate?.setTimeout) {\n return delegate.setTimeout(handler, timeout, ...args);\n }\n return setTimeout(handler, timeout, ...args);\n },\n clearTimeout(handle) {\n const { delegate } = timeoutProvider;\n return (delegate?.clearTimeout || clearTimeout)(handle as any);\n },\n delegate: undefined,\n};\n", "import { config } from '../config';\nimport { timeoutProvider } from '../scheduler/timeoutProvider';\n\n/**\n * Handles an error on another job either with the user-configured {@link onUnhandledError},\n * or by throwing it on that new job so it can be picked up by `window.onerror`, `process.on('error')`, etc.\n *\n * This should be called whenever there is an error that is out-of-band with the subscription\n * or when an error hits a terminal boundary of the subscription and no error handler was provided.\n *\n * @param err the error to report\n */\nexport function reportUnhandledError(err: any) {\n timeoutProvider.setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n // Execute the user-configured error handler.\n onUnhandledError(err);\n } else {\n // Throw so it is picked up by the runtime's uncaught error mechanism.\n throw err;\n }\n });\n}\n", "/* tslint:disable:no-empty */\nexport function noop() { }\n", "import { CompleteNotification, NextNotification, ErrorNotification } from './types';\n\n/**\n * A completion object optimized for memory use and created to be the\n * same \"shape\" as other notifications in v8.\n * @internal\n */\nexport const COMPLETE_NOTIFICATION = (() => createNotification('C', undefined, undefined) as CompleteNotification)();\n\n/**\n * Internal use only. Creates an optimized error notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function errorNotification(error: any): ErrorNotification {\n return createNotification('E', undefined, error) as any;\n}\n\n/**\n * Internal use only. Creates an optimized next notification that is the same \"shape\"\n * as other notifications.\n * @internal\n */\nexport function nextNotification(value: T) {\n return createNotification('N', value, undefined) as NextNotification;\n}\n\n/**\n * Ensures that all notifications created internally have the same \"shape\" in v8.\n *\n * TODO: This is only exported to support a crazy legacy test in `groupBy`.\n * @internal\n */\nexport function createNotification(kind: 'N' | 'E' | 'C', value: any, error: any) {\n return {\n kind,\n value,\n error,\n };\n}\n", "import { config } from '../config';\n\nlet context: { errorThrown: boolean; error: any } | null = null;\n\n/**\n * Handles dealing with errors for super-gross mode. Creates a context, in which\n * any synchronously thrown errors will be passed to {@link captureError}. Which\n * will record the error such that it will be rethrown after the call back is complete.\n * TODO: Remove in v8\n * @param cb An immediately executed function.\n */\nexport function errorContext(cb: () => void) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n const isRoot = !context;\n if (isRoot) {\n context = { errorThrown: false, error: null };\n }\n cb();\n if (isRoot) {\n const { errorThrown, error } = context!;\n context = null;\n if (errorThrown) {\n throw error;\n }\n }\n } else {\n // This is the general non-deprecated path for everyone that\n // isn't crazy enough to use super-gross mode (useDeprecatedSynchronousErrorHandling)\n cb();\n }\n}\n\n/**\n * Captures errors only in super-gross mode.\n * @param err the error to capture\n */\nexport function captureError(err: any) {\n if (config.useDeprecatedSynchronousErrorHandling && context) {\n context.errorThrown = true;\n context.error = err;\n }\n}\n", "import { isFunction } from './util/isFunction';\nimport { Observer, ObservableNotification } from './types';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nimport { nextNotification, errorNotification, COMPLETE_NOTIFICATION } from './NotificationFactories';\nimport { timeoutProvider } from './scheduler/timeoutProvider';\nimport { captureError } from './util/errorContext';\n\n/**\n * Implements the {@link Observer} interface and extends the\n * {@link Subscription} class. While the {@link Observer} is the public API for\n * consuming the values of an {@link Observable}, all Observers get converted to\n * a Subscriber, in order to provide Subscription-like capabilities such as\n * `unsubscribe`. Subscriber is a common type in RxJS, and crucial for\n * implementing operators, but it is rarely used as a public API.\n *\n * @class Subscriber\n */\nexport class Subscriber extends Subscription implements Observer {\n /**\n * A static factory for a Subscriber, given a (potentially partial) definition\n * of an Observer.\n * @param next The `next` callback of an Observer.\n * @param error The `error` callback of an\n * Observer.\n * @param complete The `complete` callback of an\n * Observer.\n * @return A Subscriber wrapping the (partially defined)\n * Observer represented by the given arguments.\n * @nocollapse\n * @deprecated Do not use. Will be removed in v8. There is no replacement for this\n * method, and there is no reason to be creating instances of `Subscriber` directly.\n * If you have a specific use case, please file an issue.\n */\n static create(next?: (x?: T) => void, error?: (e?: any) => void, complete?: () => void): Subscriber {\n return new SafeSubscriber(next, error, complete);\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected isStopped: boolean = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n protected destination: Subscriber | Observer; // this `any` is the escape hatch to erase extra type param (e.g. R)\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * There is no reason to directly create an instance of Subscriber. This type is exported for typings reasons.\n */\n constructor(destination?: Subscriber | Observer) {\n super();\n if (destination) {\n this.destination = destination;\n // Automatically chain subscriptions together here.\n // if destination is a Subscription, then it is a Subscriber.\n if (isSubscription(destination)) {\n destination.add(this);\n }\n } else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `next` from\n * the Observable, with a value. The Observable may call this method 0 or more\n * times.\n * @param {T} [value] The `next` value.\n * @return {void}\n */\n next(value?: T): void {\n if (this.isStopped) {\n handleStoppedNotification(nextNotification(value), this);\n } else {\n this._next(value!);\n }\n }\n\n /**\n * The {@link Observer} callback to receive notifications of type `error` from\n * the Observable, with an attached `Error`. Notifies the Observer that\n * the Observable has experienced an error condition.\n * @param {any} [err] The `error` exception.\n * @return {void}\n */\n error(err?: any): void {\n if (this.isStopped) {\n handleStoppedNotification(errorNotification(err), this);\n } else {\n this.isStopped = true;\n this._error(err);\n }\n }\n\n /**\n * The {@link Observer} callback to receive a valueless notification of type\n * `complete` from the Observable. Notifies the Observer that the Observable\n * has finished sending push-based notifications.\n * @return {void}\n */\n complete(): void {\n if (this.isStopped) {\n handleStoppedNotification(COMPLETE_NOTIFICATION, this);\n } else {\n this.isStopped = true;\n this._complete();\n }\n }\n\n unsubscribe(): void {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n this.destination = null!;\n }\n }\n\n protected _next(value: T): void {\n this.destination.next(value);\n }\n\n protected _error(err: any): void {\n try {\n this.destination.error(err);\n } finally {\n this.unsubscribe();\n }\n }\n\n protected _complete(): void {\n try {\n this.destination.complete();\n } finally {\n this.unsubscribe();\n }\n }\n}\n\n/**\n * This bind is captured here because we want to be able to have\n * compatibility with monoid libraries that tend to use a method named\n * `bind`. In particular, a library called Monio requires this.\n */\nconst _bind = Function.prototype.bind;\n\nfunction bind any>(fn: Fn, thisArg: any): Fn {\n return _bind.call(fn, thisArg);\n}\n\n/**\n * Internal optimization only, DO NOT EXPOSE.\n * @internal\n */\nclass ConsumerObserver implements Observer {\n constructor(private partialObserver: Partial>) {}\n\n next(value: T): void {\n const { partialObserver } = this;\n if (partialObserver.next) {\n try {\n partialObserver.next(value);\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n\n error(err: any): void {\n const { partialObserver } = this;\n if (partialObserver.error) {\n try {\n partialObserver.error(err);\n } catch (error) {\n handleUnhandledError(error);\n }\n } else {\n handleUnhandledError(err);\n }\n }\n\n complete(): void {\n const { partialObserver } = this;\n if (partialObserver.complete) {\n try {\n partialObserver.complete();\n } catch (error) {\n handleUnhandledError(error);\n }\n }\n }\n}\n\nexport class SafeSubscriber extends Subscriber {\n constructor(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((e?: any) => void) | null,\n complete?: (() => void) | null\n ) {\n super();\n\n let partialObserver: Partial>;\n if (isFunction(observerOrNext) || !observerOrNext) {\n // The first argument is a function, not an observer. The next\n // two arguments *could* be observers, or they could be empty.\n partialObserver = {\n next: (observerOrNext ?? undefined) as (((value: T) => void) | undefined),\n error: error ?? undefined,\n complete: complete ?? undefined,\n };\n } else {\n // The first argument is a partial observer.\n let context: any;\n if (this && config.useDeprecatedNextContext) {\n // This is a deprecated path that made `this.unsubscribe()` available in\n // next handler functions passed to subscribe. This only exists behind a flag\n // now, as it is *very* slow.\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n partialObserver = {\n next: observerOrNext.next && bind(observerOrNext.next, context),\n error: observerOrNext.error && bind(observerOrNext.error, context),\n complete: observerOrNext.complete && bind(observerOrNext.complete, context),\n };\n } else {\n // The \"normal\" path. Just use the partial observer directly.\n partialObserver = observerOrNext;\n }\n }\n\n // Wrap the partial observer to ensure it's a full observer, and\n // make sure proper error handling is accounted for.\n this.destination = new ConsumerObserver(partialObserver);\n }\n}\n\nfunction handleUnhandledError(error: any) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n captureError(error);\n } else {\n // Ideal path, we report this as an unhandled error,\n // which is thrown on a new call stack.\n reportUnhandledError(error);\n }\n}\n\n/**\n * An error handler used when no error handler was supplied\n * to the SafeSubscriber -- meaning no error handler was supplied\n * do the `subscribe` call on our observable.\n * @param err The error to handle\n */\nfunction defaultErrorHandler(err: any) {\n throw err;\n}\n\n/**\n * A handler for notifications that cannot be sent to a stopped subscriber.\n * @param notification The notification being sent\n * @param subscriber The stopped subscriber\n */\nfunction handleStoppedNotification(notification: ObservableNotification, subscriber: Subscriber) {\n const { onStoppedNotification } = config;\n onStoppedNotification && timeoutProvider.setTimeout(() => onStoppedNotification(notification, subscriber));\n}\n\n/**\n * The observer used as a stub for subscriptions where the user did not\n * pass any arguments to `subscribe`. Comes with the default error handling\n * behavior.\n */\nexport const EMPTY_OBSERVER: Readonly> & { closed: true } = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n", "/**\n * Symbol.observable or a string \"@@observable\". Used for interop\n *\n * @deprecated We will no longer be exporting this symbol in upcoming versions of RxJS.\n * Instead polyfill and use Symbol.observable directly *or* use https://www.npmjs.com/package/symbol-observable\n */\nexport const observable: string | symbol = (() => (typeof Symbol === 'function' && Symbol.observable) || '@@observable')();\n", "/**\n * This function takes one parameter and just returns it. Simply put,\n * this is like `(x: T): T => x`.\n *\n * ## Examples\n *\n * This is useful in some cases when using things like `mergeMap`\n *\n * ```ts\n * import { interval, take, map, range, mergeMap, identity } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(5));\n *\n * const result$ = source$.pipe(\n * map(i => range(i)),\n * mergeMap(identity) // same as mergeMap(x => x)\n * );\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * Or when you want to selectively apply an operator\n *\n * ```ts\n * import { interval, take, identity } from 'rxjs';\n *\n * const shouldLimit = () => Math.random() < 0.5;\n *\n * const source$ = interval(1000);\n *\n * const result$ = source$.pipe(shouldLimit() ? take(5) : identity);\n *\n * result$.subscribe({\n * next: console.log\n * });\n * ```\n *\n * @param x Any value that is returned by this function\n * @returns The value passed as the first parameter to this function\n */\nexport function identity(x: T): T {\n return x;\n}\n", "import { identity } from './identity';\nimport { UnaryFunction } from '../types';\n\nexport function pipe(): typeof identity;\nexport function pipe(fn1: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction): UnaryFunction;\nexport function pipe(fn1: UnaryFunction, fn2: UnaryFunction, fn3: UnaryFunction): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction\n): UnaryFunction;\nexport function pipe(\n fn1: UnaryFunction,\n fn2: UnaryFunction,\n fn3: UnaryFunction,\n fn4: UnaryFunction,\n fn5: UnaryFunction,\n fn6: UnaryFunction,\n fn7: UnaryFunction,\n fn8: UnaryFunction,\n fn9: UnaryFunction,\n ...fns: UnaryFunction[]\n): UnaryFunction;\n\n/**\n * pipe() can be called on one or more functions, each of which can take one argument (\"UnaryFunction\")\n * and uses it to return a value.\n * It returns a function that takes one argument, passes it to the first UnaryFunction, and then\n * passes the result to the next one, passes that result to the next one, and so on. \n */\nexport function pipe(...fns: Array>): UnaryFunction {\n return pipeFromArray(fns);\n}\n\n/** @internal */\nexport function pipeFromArray(fns: Array>): UnaryFunction {\n if (fns.length === 0) {\n return identity as UnaryFunction;\n }\n\n if (fns.length === 1) {\n return fns[0];\n }\n\n return function piped(input: T): R {\n return fns.reduce((prev: any, fn: UnaryFunction) => fn(prev), input as any);\n };\n}\n", "import { Operator } from './Operator';\nimport { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription, Subscription } from './Subscription';\nimport { TeardownLogic, OperatorFunction, Subscribable, Observer } from './types';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { isFunction } from './util/isFunction';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A representation of any set of values over any amount of time. This is the most basic building block\n * of RxJS.\n *\n * @class Observable\n */\nexport class Observable implements Subscribable {\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n source: Observable | undefined;\n\n /**\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n */\n operator: Operator | undefined;\n\n /**\n * @constructor\n * @param {Function} subscribe the function that is called when the Observable is\n * initially subscribed to. This function is given a Subscriber, to which new values\n * can be `next`ed, or an `error` method can be called to raise an error, or\n * `complete` can be called to notify of a successful completion.\n */\n constructor(subscribe?: (this: Observable, subscriber: Subscriber) => TeardownLogic) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n\n // HACK: Since TypeScript inherits static properties too, we have to\n // fight against TypeScript here so Subject can have a different static create signature\n /**\n * Creates a new Observable by calling the Observable constructor\n * @owner Observable\n * @method create\n * @param {Function} subscribe? the subscriber function to be passed to the Observable constructor\n * @return {Observable} a new observable\n * @nocollapse\n * @deprecated Use `new Observable()` instead. Will be removed in v8.\n */\n static create: (...args: any[]) => any = (subscribe?: (subscriber: Subscriber) => TeardownLogic) => {\n return new Observable(subscribe);\n };\n\n /**\n * Creates a new Observable, with this Observable instance as the source, and the passed\n * operator defined as the new observable's operator.\n * @method lift\n * @param operator the operator defining the operation to take on the observable\n * @return a new observable with the Operator applied\n * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8.\n * If you have implemented an operator using `lift`, it is recommended that you create an\n * operator by simply returning `new Observable()` directly. See \"Creating new operators from\n * scratch\" section here: https://rxjs.dev/guide/operators\n */\n lift(operator?: Operator): Observable {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n\n subscribe(observerOrNext?: Partial> | ((value: T) => void)): Subscription;\n /** @deprecated Instead of passing separate callback arguments, use an observer argument. Signatures taking separate callback arguments will be removed in v8. Details: https://rxjs.dev/deprecations/subscribe-arguments */\n subscribe(next?: ((value: T) => void) | null, error?: ((error: any) => void) | null, complete?: (() => void) | null): Subscription;\n /**\n * Invokes an execution of an Observable and registers Observer handlers for notifications it will emit.\n *\n * Use it when you have all these Observables, but still nothing is happening.\n *\n * `subscribe` is not a regular operator, but a method that calls Observable's internal `subscribe` function. It\n * might be for example a function that you passed to Observable's constructor, but most of the time it is\n * a library implementation, which defines what will be emitted by an Observable, and when it be will emitted. This means\n * that calling `subscribe` is actually the moment when Observable starts its work, not when it is created, as it is often\n * the thought.\n *\n * Apart from starting the execution of an Observable, this method allows you to listen for values\n * that an Observable emits, as well as for when it completes or errors. You can achieve this in two\n * of the following ways.\n *\n * The first way is creating an object that implements {@link Observer} interface. It should have methods\n * defined by that interface, but note that it should be just a regular JavaScript object, which you can create\n * yourself in any way you want (ES6 class, classic function constructor, object literal etc.). In particular, do\n * not attempt to use any RxJS implementation details to create Observers - you don't need them. Remember also\n * that your object does not have to implement all methods. If you find yourself creating a method that doesn't\n * do anything, you can simply omit it. Note however, if the `error` method is not provided and an error happens,\n * it will be thrown asynchronously. Errors thrown asynchronously cannot be caught using `try`/`catch`. Instead,\n * use the {@link onUnhandledError} configuration option or use a runtime handler (like `window.onerror` or\n * `process.on('error)`) to be notified of unhandled errors. Because of this, it's recommended that you provide\n * an `error` method to avoid missing thrown errors.\n *\n * The second way is to give up on Observer object altogether and simply provide callback functions in place of its methods.\n * This means you can provide three functions as arguments to `subscribe`, where the first function is equivalent\n * of a `next` method, the second of an `error` method and the third of a `complete` method. Just as in case of an Observer,\n * if you do not need to listen for something, you can omit a function by passing `undefined` or `null`,\n * since `subscribe` recognizes these functions by where they were placed in function call. When it comes\n * to the `error` function, as with an Observer, if not provided, errors emitted by an Observable will be thrown asynchronously.\n *\n * You can, however, subscribe with no parameters at all. This may be the case where you're not interested in terminal events\n * and you also handled emissions internally by using operators (e.g. using `tap`).\n *\n * Whichever style of calling `subscribe` you use, in both cases it returns a Subscription object.\n * This object allows you to call `unsubscribe` on it, which in turn will stop the work that an Observable does and will clean\n * up all resources that an Observable used. Note that cancelling a subscription will not call `complete` callback\n * provided to `subscribe` function, which is reserved for a regular completion signal that comes from an Observable.\n *\n * Remember that callbacks provided to `subscribe` are not guaranteed to be called asynchronously.\n * It is an Observable itself that decides when these functions will be called. For example {@link of}\n * by default emits all its values synchronously. Always check documentation for how given Observable\n * will behave when subscribed and if its default behavior can be modified with a `scheduler`.\n *\n * #### Examples\n *\n * Subscribe with an {@link guide/observer Observer}\n *\n * ```ts\n * import { of } from 'rxjs';\n *\n * const sumObserver = {\n * sum: 0,\n * next(value) {\n * console.log('Adding: ' + value);\n * this.sum = this.sum + value;\n * },\n * error() {\n * // We actually could just remove this method,\n * // since we do not really care about errors right now.\n * },\n * complete() {\n * console.log('Sum equals: ' + this.sum);\n * }\n * };\n *\n * of(1, 2, 3) // Synchronously emits 1, 2, 3 and then completes.\n * .subscribe(sumObserver);\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Subscribe with functions ({@link deprecations/subscribe-arguments deprecated})\n *\n * ```ts\n * import { of } from 'rxjs'\n *\n * let sum = 0;\n *\n * of(1, 2, 3).subscribe(\n * value => {\n * console.log('Adding: ' + value);\n * sum = sum + value;\n * },\n * undefined,\n * () => console.log('Sum equals: ' + sum)\n * );\n *\n * // Logs:\n * // 'Adding: 1'\n * // 'Adding: 2'\n * // 'Adding: 3'\n * // 'Sum equals: 6'\n * ```\n *\n * Cancel a subscription\n *\n * ```ts\n * import { interval } from 'rxjs';\n *\n * const subscription = interval(1000).subscribe({\n * next(num) {\n * console.log(num)\n * },\n * complete() {\n * // Will not be called, even when cancelling subscription.\n * console.log('completed!');\n * }\n * });\n *\n * setTimeout(() => {\n * subscription.unsubscribe();\n * console.log('unsubscribed!');\n * }, 2500);\n *\n * // Logs:\n * // 0 after 1s\n * // 1 after 2s\n * // 'unsubscribed!' after 2.5s\n * ```\n *\n * @param {Observer|Function} observerOrNext (optional) Either an observer with methods to be called,\n * or the first of three possible handlers, which is the handler for each value emitted from the subscribed\n * Observable.\n * @param {Function} error (optional) A handler for a terminal event resulting from an error. If no error handler is provided,\n * the error will be thrown asynchronously as unhandled.\n * @param {Function} complete (optional) A handler for a terminal event resulting from successful completion.\n * @return {Subscription} a subscription reference to the registered handlers\n * @method subscribe\n */\n subscribe(\n observerOrNext?: Partial> | ((value: T) => void) | null,\n error?: ((error: any) => void) | null,\n complete?: (() => void) | null\n ): Subscription {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n\n errorContext(() => {\n const { operator, source } = this;\n subscriber.add(\n operator\n ? // We're dealing with a subscription in the\n // operator chain to one of our lifted operators.\n operator.call(subscriber, source)\n : source\n ? // If `source` has a value, but `operator` does not, something that\n // had intimate knowledge of our API, like our `Subject`, must have\n // set it. We're going to just call `_subscribe` directly.\n this._subscribe(subscriber)\n : // In all other cases, we're likely wrapping a user-provided initializer\n // function, so we need to catch errors and handle them appropriately.\n this._trySubscribe(subscriber)\n );\n });\n\n return subscriber;\n }\n\n /** @internal */\n protected _trySubscribe(sink: Subscriber): TeardownLogic {\n try {\n return this._subscribe(sink);\n } catch (err) {\n // We don't need to return anything in this case,\n // because it's just going to try to `add()` to a subscription\n // above.\n sink.error(err);\n }\n }\n\n /**\n * Used as a NON-CANCELLABLE means of subscribing to an observable, for use with\n * APIs that expect promises, like `async/await`. You cannot unsubscribe from this.\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * #### Example\n *\n * ```ts\n * import { interval, take } from 'rxjs';\n *\n * const source$ = interval(1000).pipe(take(4));\n *\n * async function getTotal() {\n * let total = 0;\n *\n * await source$.forEach(value => {\n * total += value;\n * console.log('observable -> ' + value);\n * });\n *\n * return total;\n * }\n *\n * getTotal().then(\n * total => console.log('Total: ' + total)\n * );\n *\n * // Expected:\n * // 'observable -> 0'\n * // 'observable -> 1'\n * // 'observable -> 2'\n * // 'observable -> 3'\n * // 'Total: 6'\n * ```\n *\n * @param next a handler for each value emitted by the observable\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n */\n forEach(next: (value: T) => void): Promise;\n\n /**\n * @param next a handler for each value emitted by the observable\n * @param promiseCtor a constructor function used to instantiate the Promise\n * @return a promise that either resolves on observable completion or\n * rejects with the handled error\n * @deprecated Passing a Promise constructor will no longer be available\n * in upcoming versions of RxJS. This is because it adds weight to the library, for very\n * little benefit. If you need this functionality, it is recommended that you either\n * polyfill Promise, or you create an adapter to convert the returned native promise\n * to whatever promise implementation you wanted. Will be removed in v8.\n */\n forEach(next: (value: T) => void, promiseCtor: PromiseConstructorLike): Promise;\n\n forEach(next: (value: T) => void, promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n const subscriber = new SafeSubscriber({\n next: (value) => {\n try {\n next(value);\n } catch (err) {\n reject(err);\n subscriber.unsubscribe();\n }\n },\n error: reject,\n complete: resolve,\n });\n this.subscribe(subscriber);\n }) as Promise;\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): TeardownLogic {\n return this.source?.subscribe(subscriber);\n }\n\n /**\n * An interop point defined by the es7-observable spec https://github.com/zenparsing/es-observable\n * @method Symbol.observable\n * @return {Observable} this instance of the observable\n */\n [Symbol_observable]() {\n return this;\n }\n\n /* tslint:disable:max-line-length */\n pipe(): Observable;\n pipe(op1: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction): Observable;\n pipe(op1: OperatorFunction, op2: OperatorFunction, op3: OperatorFunction): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction\n ): Observable;\n pipe(\n op1: OperatorFunction,\n op2: OperatorFunction,\n op3: OperatorFunction,\n op4: OperatorFunction,\n op5: OperatorFunction,\n op6: OperatorFunction,\n op7: OperatorFunction,\n op8: OperatorFunction,\n op9: OperatorFunction,\n ...operations: OperatorFunction[]\n ): Observable;\n /* tslint:enable:max-line-length */\n\n /**\n * Used to stitch together functional operators into a chain.\n * @method pipe\n * @return {Observable} the Observable result of all of the operators having\n * been called in the order they were passed in.\n *\n * ## Example\n *\n * ```ts\n * import { interval, filter, map, scan } from 'rxjs';\n *\n * interval(1000)\n * .pipe(\n * filter(x => x % 2 === 0),\n * map(x => x + x),\n * scan((acc, x) => acc + x)\n * )\n * .subscribe(x => console.log(x));\n * ```\n */\n pipe(...operations: OperatorFunction[]): Observable {\n return pipeFromArray(operations)(this);\n }\n\n /* tslint:disable:max-line-length */\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: typeof Promise): Promise;\n /** @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise */\n toPromise(PromiseCtor: PromiseConstructorLike): Promise;\n /* tslint:enable:max-line-length */\n\n /**\n * Subscribe to this Observable and get a Promise resolving on\n * `complete` with the last emission (if any).\n *\n * **WARNING**: Only use this with observables you *know* will complete. If the source\n * observable does not complete, you will end up with a promise that is hung up, and\n * potentially all of the state of an async function hanging out in memory. To avoid\n * this situation, look into adding something like {@link timeout}, {@link take},\n * {@link takeWhile}, or {@link takeUntil} amongst others.\n *\n * @method toPromise\n * @param [promiseCtor] a constructor function used to instantiate\n * the Promise\n * @return A Promise that resolves with the last value emit, or\n * rejects on an error. If there were no emissions, Promise\n * resolves with undefined.\n * @deprecated Replaced with {@link firstValueFrom} and {@link lastValueFrom}. Will be removed in v8. Details: https://rxjs.dev/deprecations/to-promise\n */\n toPromise(promiseCtor?: PromiseConstructorLike): Promise {\n promiseCtor = getPromiseCtor(promiseCtor);\n\n return new promiseCtor((resolve, reject) => {\n let value: T | undefined;\n this.subscribe(\n (x: T) => (value = x),\n (err: any) => reject(err),\n () => resolve(value)\n );\n }) as Promise;\n }\n}\n\n/**\n * Decides between a passed promise constructor from consuming code,\n * A default configured promise constructor, and the native promise\n * constructor and returns it. If nothing can be found, it will throw\n * an error.\n * @param promiseCtor The optional promise constructor to passed by consuming code\n */\nfunction getPromiseCtor(promiseCtor: PromiseConstructorLike | undefined) {\n return promiseCtor ?? config.Promise ?? Promise;\n}\n\nfunction isObserver(value: any): value is Observer {\n return value && isFunction(value.next) && isFunction(value.error) && isFunction(value.complete);\n}\n\nfunction isSubscriber(value: any): value is Subscriber {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n", "import { Observable } from '../Observable';\nimport { Subscriber } from '../Subscriber';\nimport { OperatorFunction } from '../types';\nimport { isFunction } from './isFunction';\n\n/**\n * Used to determine if an object is an Observable with a lift function.\n */\nexport function hasLift(source: any): source is { lift: InstanceType['lift'] } {\n return isFunction(source?.lift);\n}\n\n/**\n * Creates an `OperatorFunction`. Used to define operators throughout the library in a concise way.\n * @param init The logic to connect the liftedSource to the subscriber at the moment of subscription.\n */\nexport function operate(\n init: (liftedSource: Observable, subscriber: Subscriber) => (() => void) | void\n): OperatorFunction {\n return (source: Observable) => {\n if (hasLift(source)) {\n return source.lift(function (this: Subscriber, liftedSource: Observable) {\n try {\n return init(liftedSource, this);\n } catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n", "import { Subscriber } from '../Subscriber';\n\n/**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional teardown logic here. This will only be called on teardown if the\n * subscriber itself is not already closed. This is called after all other teardown logic is executed.\n */\nexport function createOperatorSubscriber(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n onFinalize?: () => void\n): Subscriber {\n return new OperatorSubscriber(destination, onNext, onComplete, onError, onFinalize);\n}\n\n/**\n * A generic helper for allowing operators to be created with a Subscriber and\n * use closures to capture necessary state from the operator function itself.\n */\nexport class OperatorSubscriber extends Subscriber {\n /**\n * Creates an instance of an `OperatorSubscriber`.\n * @param destination The downstream subscriber.\n * @param onNext Handles next values, only called if this subscriber is not stopped or closed. Any\n * error that occurs in this function is caught and sent to the `error` method of this subscriber.\n * @param onError Handles errors from the subscription, any errors that occur in this handler are caught\n * and send to the `destination` error handler.\n * @param onComplete Handles completion notification from the subscription. Any errors that occur in\n * this handler are sent to the `destination` error handler.\n * @param onFinalize Additional finalization logic here. This will only be called on finalization if the\n * subscriber itself is not already closed. This is called after all other finalization logic is executed.\n * @param shouldUnsubscribe An optional check to see if an unsubscribe call should truly unsubscribe.\n * NOTE: This currently **ONLY** exists to support the strange behavior of {@link groupBy}, where unsubscription\n * to the resulting observable does not actually disconnect from the source if there are active subscriptions\n * to any grouped observable. (DO NOT EXPOSE OR USE EXTERNALLY!!!)\n */\n constructor(\n destination: Subscriber,\n onNext?: (value: T) => void,\n onComplete?: () => void,\n onError?: (err: any) => void,\n private onFinalize?: () => void,\n private shouldUnsubscribe?: () => boolean\n ) {\n // It's important - for performance reasons - that all of this class's\n // members are initialized and that they are always initialized in the same\n // order. This will ensure that all OperatorSubscriber instances have the\n // same hidden class in V8. This, in turn, will help keep the number of\n // hidden classes involved in property accesses within the base class as\n // low as possible. If the number of hidden classes involved exceeds four,\n // the property accesses will become megamorphic and performance penalties\n // will be incurred - i.e. inline caches won't be used.\n //\n // The reasons for ensuring all instances have the same hidden class are\n // further discussed in this blog post from Benedikt Meurer:\n // https://benediktmeurer.de/2018/03/23/impact-of-polymorphism-on-component-based-frameworks-like-react/\n super(destination);\n this._next = onNext\n ? function (this: OperatorSubscriber, value: T) {\n try {\n onNext(value);\n } catch (err) {\n destination.error(err);\n }\n }\n : super._next;\n this._error = onError\n ? function (this: OperatorSubscriber, err: any) {\n try {\n onError(err);\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._error;\n this._complete = onComplete\n ? function (this: OperatorSubscriber) {\n try {\n onComplete();\n } catch (err) {\n // Send any errors that occur down stream.\n destination.error(err);\n } finally {\n // Ensure finalization.\n this.unsubscribe();\n }\n }\n : super._complete;\n }\n\n unsubscribe() {\n if (!this.shouldUnsubscribe || this.shouldUnsubscribe()) {\n const { closed } = this;\n super.unsubscribe();\n // Execute additional teardown if we have any and we didn't already do so.\n !closed && this.onFinalize?.();\n }\n }\n}\n", "import { Subscription } from '../Subscription';\n\ninterface AnimationFrameProvider {\n schedule(callback: FrameRequestCallback): Subscription;\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n delegate:\n | {\n requestAnimationFrame: typeof requestAnimationFrame;\n cancelAnimationFrame: typeof cancelAnimationFrame;\n }\n | undefined;\n}\n\nexport const animationFrameProvider: AnimationFrameProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel: typeof cancelAnimationFrame | undefined = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n // Clear the cancel function. The request has been fulfilled, so\n // attempting to cancel the request upon unsubscription would be\n // pointless.\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel?.(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.requestAnimationFrame || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return (delegate?.cancelAnimationFrame || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n", "import { createErrorClass } from './createErrorClass';\n\nexport interface ObjectUnsubscribedError extends Error {}\n\nexport interface ObjectUnsubscribedErrorCtor {\n /**\n * @deprecated Internal implementation detail. Do not construct error instances.\n * Cannot be tagged as internal: https://github.com/ReactiveX/rxjs/issues/6269\n */\n new (): ObjectUnsubscribedError;\n}\n\n/**\n * An error thrown when an action is invalid because the object has been\n * unsubscribed.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n *\n * @class ObjectUnsubscribedError\n */\nexport const ObjectUnsubscribedError: ObjectUnsubscribedErrorCtor = createErrorClass(\n (_super) =>\n function ObjectUnsubscribedErrorImpl(this: any) {\n _super(this);\n this.name = 'ObjectUnsubscribedError';\n this.message = 'object unsubscribed';\n }\n);\n", "import { Operator } from './Operator';\nimport { Observable } from './Observable';\nimport { Subscriber } from './Subscriber';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { Observer, SubscriptionLike, TeardownLogic } from './types';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nimport { errorContext } from './util/errorContext';\n\n/**\n * A Subject is a special type of Observable that allows values to be\n * multicasted to many Observers. Subjects are like EventEmitters.\n *\n * Every Subject is an Observable and an Observer. You can subscribe to a\n * Subject, and you can call next to feed values as well as error and complete.\n */\nexport class Subject extends Observable implements SubscriptionLike {\n closed = false;\n\n private currentObservers: Observer[] | null = null;\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n observers: Observer[] = [];\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n isStopped = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n hasError = false;\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n thrownError: any = null;\n\n /**\n * Creates a \"subject\" by basically gluing an observer to an observable.\n *\n * @nocollapse\n * @deprecated Recommended you do not use. Will be removed at some point in the future. Plans for replacement still under discussion.\n */\n static create: (...args: any[]) => any = (destination: Observer, source: Observable): AnonymousSubject => {\n return new AnonymousSubject(destination, source);\n };\n\n constructor() {\n // NOTE: This must be here to obscure Observable's constructor.\n super();\n }\n\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n lift(operator: Operator): Observable {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator as any;\n return subject as any;\n }\n\n /** @internal */\n protected _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n\n next(value: T) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n if (!this.currentObservers) {\n this.currentObservers = Array.from(this.observers);\n }\n for (const observer of this.currentObservers) {\n observer.next(value);\n }\n }\n });\n }\n\n error(err: any) {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.error(err);\n }\n }\n });\n }\n\n complete() {\n errorContext(() => {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift()!.complete();\n }\n }\n });\n }\n\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = this.currentObservers = null!;\n }\n\n get observed() {\n return this.observers?.length > 0;\n }\n\n /** @internal */\n protected _trySubscribe(subscriber: Subscriber): TeardownLogic {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n\n /** @internal */\n protected _innerSubscribe(subscriber: Subscriber) {\n const { hasError, isStopped, observers } = this;\n if (hasError || isStopped) {\n return EMPTY_SUBSCRIPTION;\n }\n this.currentObservers = null;\n observers.push(subscriber);\n return new Subscription(() => {\n this.currentObservers = null;\n arrRemove(observers, subscriber);\n });\n }\n\n /** @internal */\n protected _checkFinalizedStatuses(subscriber: Subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n } else if (isStopped) {\n subscriber.complete();\n }\n }\n\n /**\n * Creates a new Observable with this Subject as the source. You can do this\n * to create custom Observer-side logic of the Subject and conceal it from\n * code that uses the Observable.\n * @return {Observable} Observable that the Subject casts to\n */\n asObservable(): Observable {\n const observable: any = new Observable();\n observable.source = this;\n return observable;\n }\n}\n\n/**\n * @class AnonymousSubject\n */\nexport class AnonymousSubject extends Subject {\n constructor(\n /** @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */\n public destination?: Observer,\n source?: Observable\n ) {\n super();\n this.source = source;\n }\n\n next(value: T) {\n this.destination?.next?.(value);\n }\n\n error(err: any) {\n this.destination?.error?.(err);\n }\n\n complete() {\n this.destination?.complete?.();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n return this.source?.subscribe(subscriber) ?? EMPTY_SUBSCRIPTION;\n }\n}\n", "import { Subject } from './Subject';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\n\n/**\n * A variant of Subject that requires an initial value and emits its current\n * value whenever it is subscribed to.\n *\n * @class BehaviorSubject\n */\nexport class BehaviorSubject extends Subject {\n constructor(private _value: T) {\n super();\n }\n\n get value(): T {\n return this.getValue();\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n\n getValue(): T {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n\n next(value: T): void {\n super.next((this._value = value));\n }\n}\n", "import { TimestampProvider } from '../types';\n\ninterface DateTimestampProvider extends TimestampProvider {\n delegate: TimestampProvider | undefined;\n}\n\nexport const dateTimestampProvider: DateTimestampProvider = {\n now() {\n // Use the variable rather than `this` so that the function can be called\n // without being bound to the provider.\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n", "import { Subject } from './Subject';\nimport { TimestampProvider } from './types';\nimport { Subscriber } from './Subscriber';\nimport { Subscription } from './Subscription';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * A variant of {@link Subject} that \"replays\" old values to new subscribers by emitting them when they first subscribe.\n *\n * `ReplaySubject` has an internal buffer that will store a specified number of values that it has observed. Like `Subject`,\n * `ReplaySubject` \"observes\" values by having them passed to its `next` method. When it observes a value, it will store that\n * value for a time determined by the configuration of the `ReplaySubject`, as passed to its constructor.\n *\n * When a new subscriber subscribes to the `ReplaySubject` instance, it will synchronously emit all values in its buffer in\n * a First-In-First-Out (FIFO) manner. The `ReplaySubject` will also complete, if it has observed completion; and it will\n * error if it has observed an error.\n *\n * There are two main configuration items to be concerned with:\n *\n * 1. `bufferSize` - This will determine how many items are stored in the buffer, defaults to infinite.\n * 2. `windowTime` - The amount of time to hold a value in the buffer before removing it from the buffer.\n *\n * Both configurations may exist simultaneously. So if you would like to buffer a maximum of 3 values, as long as the values\n * are less than 2 seconds old, you could do so with a `new ReplaySubject(3, 2000)`.\n *\n * ### Differences with BehaviorSubject\n *\n * `BehaviorSubject` is similar to `new ReplaySubject(1)`, with a couple of exceptions:\n *\n * 1. `BehaviorSubject` comes \"primed\" with a single value upon construction.\n * 2. `ReplaySubject` will replay values, even after observing an error, where `BehaviorSubject` will not.\n *\n * @see {@link Subject}\n * @see {@link BehaviorSubject}\n * @see {@link shareReplay}\n */\nexport class ReplaySubject extends Subject {\n private _buffer: (T | number)[] = [];\n private _infiniteTimeWindow = true;\n\n /**\n * @param bufferSize The size of the buffer to replay on subscription\n * @param windowTime The amount of time the buffered items will stay buffered\n * @param timestampProvider An object with a `now()` method that provides the current timestamp. This is used to\n * calculate the amount of time something has been buffered.\n */\n constructor(\n private _bufferSize = Infinity,\n private _windowTime = Infinity,\n private _timestampProvider: TimestampProvider = dateTimestampProvider\n ) {\n super();\n this._infiniteTimeWindow = _windowTime === Infinity;\n this._bufferSize = Math.max(1, _bufferSize);\n this._windowTime = Math.max(1, _windowTime);\n }\n\n next(value: T): void {\n const { isStopped, _buffer, _infiniteTimeWindow, _timestampProvider, _windowTime } = this;\n if (!isStopped) {\n _buffer.push(value);\n !_infiniteTimeWindow && _buffer.push(_timestampProvider.now() + _windowTime);\n }\n this._trimBuffer();\n super.next(value);\n }\n\n /** @internal */\n protected _subscribe(subscriber: Subscriber): Subscription {\n this._throwIfClosed();\n this._trimBuffer();\n\n const subscription = this._innerSubscribe(subscriber);\n\n const { _infiniteTimeWindow, _buffer } = this;\n // We use a copy here, so reentrant code does not mutate our array while we're\n // emitting it to a new subscriber.\n const copy = _buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += _infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i] as T);\n }\n\n this._checkFinalizedStatuses(subscriber);\n\n return subscription;\n }\n\n private _trimBuffer() {\n const { _bufferSize, _timestampProvider, _buffer, _infiniteTimeWindow } = this;\n // If we don't have an infinite buffer size, and we're over the length,\n // use splice to truncate the old buffer values off. Note that we have to\n // double the size for instances where we're not using an infinite time window\n // because we're storing the values and the timestamps in the same array.\n const adjustedBufferSize = (_infiniteTimeWindow ? 1 : 2) * _bufferSize;\n _bufferSize < Infinity && adjustedBufferSize < _buffer.length && _buffer.splice(0, _buffer.length - adjustedBufferSize);\n\n // Now, if we're not in an infinite time window, remove all values where the time is\n // older than what is allowed.\n if (!_infiniteTimeWindow) {\n const now = _timestampProvider.now();\n let last = 0;\n // Search the array for the first timestamp that isn't expired and\n // truncate the buffer up to that point.\n for (let i = 1; i < _buffer.length && (_buffer[i] as number) <= now; i += 2) {\n last = i;\n }\n last && _buffer.splice(0, last + 1);\n }\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Subscription } from '../Subscription';\nimport { SchedulerAction } from '../types';\n\n/**\n * A unit of work to be executed in a `scheduler`. An action is typically\n * created from within a {@link SchedulerLike} and an RxJS user does not need to concern\n * themselves about creating and manipulating an Action.\n *\n * ```ts\n * class Action extends Subscription {\n * new (scheduler: Scheduler, work: (state?: T) => void);\n * schedule(state?: T, delay: number = 0): Subscription;\n * }\n * ```\n *\n * @class Action\n */\nexport class Action extends Subscription {\n constructor(scheduler: Scheduler, work: (this: SchedulerAction, state?: T) => void) {\n super();\n }\n /**\n * Schedules this action on its parent {@link SchedulerLike} for execution. May be passed\n * some context object, `state`. May happen at some point in the future,\n * according to the `delay` parameter, if specified.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler.\n * @return {void}\n */\n public schedule(state?: T, delay: number = 0): Subscription {\n return this;\n }\n}\n", "import type { TimerHandle } from './timerHandle';\ntype SetIntervalFunction = (handler: () => void, timeout?: number, ...args: any[]) => TimerHandle;\ntype ClearIntervalFunction = (handle: TimerHandle) => void;\n\ninterface IntervalProvider {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n delegate:\n | {\n setInterval: SetIntervalFunction;\n clearInterval: ClearIntervalFunction;\n }\n | undefined;\n}\n\nexport const intervalProvider: IntervalProvider = {\n // When accessing the delegate, use the variable rather than `this` so that\n // the functions can be called without being bound to the provider.\n setInterval(handler: () => void, timeout?: number, ...args) {\n const { delegate } = intervalProvider;\n if (delegate?.setInterval) {\n return delegate.setInterval(handler, timeout, ...args);\n }\n return setInterval(handler, timeout, ...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return (delegate?.clearInterval || clearInterval)(handle as any);\n },\n delegate: undefined,\n};\n", "import { Action } from './Action';\nimport { SchedulerAction } from '../types';\nimport { Subscription } from '../Subscription';\nimport { AsyncScheduler } from './AsyncScheduler';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncAction extends Action {\n public id: TimerHandle | undefined;\n public state?: T;\n // @ts-ignore: Property has no initializer and is not definitely assigned\n public delay: number;\n protected pending: boolean = false;\n\n constructor(protected scheduler: AsyncScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (this.closed) {\n return this;\n }\n\n // Always replace the current state with the new state.\n this.state = state;\n\n const id = this.id;\n const scheduler = this.scheduler;\n\n //\n // Important implementation note:\n //\n // Actions only execute once by default, unless rescheduled from within the\n // scheduled callback. This allows us to implement single and repeat\n // actions via the same code path, without adding API surface area, as well\n // as mimic traditional recursion but across asynchronous boundaries.\n //\n // However, JS runtimes and timers distinguish between intervals achieved by\n // serial `setTimeout` calls vs. a single `setInterval` call. An interval of\n // serial `setTimeout` calls can be individually delayed, which delays\n // scheduling the next `setTimeout`, and so on. `setInterval` attempts to\n // guarantee the interval callback will be invoked more precisely to the\n // interval period, regardless of load.\n //\n // Therefore, we use `setInterval` to schedule single and repeat actions.\n // If the action reschedules itself with the same delay, the interval is not\n // canceled. If the action doesn't reschedule, or reschedules with a\n // different delay, the interval will be canceled after scheduled callback\n // execution.\n //\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n\n // Set the pending flag indicating that this action has been scheduled, or\n // has recursively rescheduled itself.\n this.pending = true;\n\n this.delay = delay;\n // If this action has already an async Id, don't request a new one.\n this.id = this.id ?? this.requestAsyncId(scheduler, this.id, delay);\n\n return this;\n }\n\n protected requestAsyncId(scheduler: AsyncScheduler, _id?: TimerHandle, delay: number = 0): TimerHandle {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n\n protected recycleAsyncId(_scheduler: AsyncScheduler, id?: TimerHandle, delay: number | null = 0): TimerHandle | undefined {\n // If this action is rescheduled with the same delay time, don't clear the interval id.\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n // Otherwise, if the action's delay time is different from the current delay,\n // or the action has been rescheduled before it's executed, clear the interval id\n if (id != null) {\n intervalProvider.clearInterval(id);\n }\n\n return undefined;\n }\n\n /**\n * Immediately executes this action and the `work` it contains.\n * @return {any}\n */\n public execute(state: T, delay: number): any {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n } else if (this.pending === false && this.id != null) {\n // Dequeue if the action didn't reschedule itself. Don't call\n // unsubscribe(), because the action could reschedule later.\n // For example:\n // ```\n // scheduler.schedule(function doWork(counter) {\n // /* ... I'm a busy worker bee ... */\n // var originalAction = this;\n // /* wait 100ms before rescheduling the action */\n // setTimeout(function () {\n // originalAction.schedule(counter + 1);\n // }, 100);\n // }, 1000);\n // ```\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n\n protected _execute(state: T, _delay: number): any {\n let errored: boolean = false;\n let errorValue: any;\n try {\n this.work(state);\n } catch (e) {\n errored = true;\n // HACK: Since code elsewhere is relying on the \"truthiness\" of the\n // return here, we can't have it return \"\" or 0 or false.\n // TODO: Clean this up when we refactor schedulers mid-version-8 or so.\n errorValue = e ? e : new Error('Scheduled action threw falsy error');\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n\n this.work = this.state = this.scheduler = null!;\n this.pending = false;\n\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n\n this.delay = null!;\n super.unsubscribe();\n }\n }\n}\n", "import { Action } from './scheduler/Action';\nimport { Subscription } from './Subscription';\nimport { SchedulerLike, SchedulerAction } from './types';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\n\n/**\n * An execution context and a data structure to order tasks and schedule their\n * execution. Provides a notion of (potentially virtual) time, through the\n * `now()` getter method.\n *\n * Each unit of work in a Scheduler is called an `Action`.\n *\n * ```ts\n * class Scheduler {\n * now(): number;\n * schedule(work, delay?, state?): Subscription;\n * }\n * ```\n *\n * @class Scheduler\n * @deprecated Scheduler is an internal implementation detail of RxJS, and\n * should not be used directly. Rather, create your own class and implement\n * {@link SchedulerLike}. Will be made internal in v8.\n */\nexport class Scheduler implements SchedulerLike {\n public static now: () => number = dateTimestampProvider.now;\n\n constructor(private schedulerActionCtor: typeof Action, now: () => number = Scheduler.now) {\n this.now = now;\n }\n\n /**\n * A getter method that returns a number representing the current time\n * (at the time this function was called) according to the scheduler's own\n * internal clock.\n * @return {number} A number that represents the current time. May or may not\n * have a relation to wall-clock time. May or may not refer to a time unit\n * (e.g. milliseconds).\n */\n public now: () => number;\n\n /**\n * Schedules a function, `work`, for execution. May happen at some point in\n * the future, according to the `delay` parameter, if specified. May be passed\n * some context object, `state`, which will be passed to the `work` function.\n *\n * The given arguments will be processed an stored as an Action object in a\n * queue of actions.\n *\n * @param {function(state: ?T): ?Subscription} work A function representing a\n * task, or some unit of work to be executed by the Scheduler.\n * @param {number} [delay] Time to wait before executing the work, where the\n * time unit is implicit and defined by the Scheduler itself.\n * @param {T} [state] Some contextual data that the `work` function uses when\n * called by the Scheduler.\n * @return {Subscription} A subscription in order to be able to unsubscribe\n * the scheduled work.\n */\n public schedule(work: (this: SchedulerAction, state?: T) => void, delay: number = 0, state?: T): Subscription {\n return new this.schedulerActionCtor(this, work).schedule(state, delay);\n }\n}\n", "import { Scheduler } from '../Scheduler';\nimport { Action } from './Action';\nimport { AsyncAction } from './AsyncAction';\nimport { TimerHandle } from './timerHandle';\n\nexport class AsyncScheduler extends Scheduler {\n public actions: Array> = [];\n /**\n * A flag to indicate whether the Scheduler is currently executing a batch of\n * queued actions.\n * @type {boolean}\n * @internal\n */\n public _active: boolean = false;\n /**\n * An internal ID used to track the latest asynchronous task such as those\n * coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and\n * others.\n * @type {any}\n * @internal\n */\n public _scheduled: TimerHandle | undefined;\n\n constructor(SchedulerAction: typeof Action, now: () => number = Scheduler.now) {\n super(SchedulerAction, now);\n }\n\n public flush(action: AsyncAction): void {\n const { actions } = this;\n\n if (this._active) {\n actions.push(action);\n return;\n }\n\n let error: any;\n this._active = true;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions.shift()!)); // exhaust the scheduler queue\n\n this._active = false;\n\n if (error) {\n while ((action = actions.shift()!)) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\n/**\n *\n * Async Scheduler\n *\n * Schedule task as if you used setTimeout(task, duration)\n *\n * `async` scheduler schedules tasks asynchronously, by putting them on the JavaScript\n * event loop queue. It is best used to delay tasks in time or to schedule tasks repeating\n * in intervals.\n *\n * If you just want to \"defer\" task, that is to perform it right after currently\n * executing synchronous code ends (commonly achieved by `setTimeout(deferredTask, 0)`),\n * better choice will be the {@link asapScheduler} scheduler.\n *\n * ## Examples\n * Use async scheduler to delay task\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * const task = () => console.log('it works!');\n *\n * asyncScheduler.schedule(task, 2000);\n *\n * // After 2 seconds logs:\n * // \"it works!\"\n * ```\n *\n * Use async scheduler to repeat task in intervals\n * ```ts\n * import { asyncScheduler } from 'rxjs';\n *\n * function task(state) {\n * console.log(state);\n * this.schedule(state + 1, 1000); // `this` references currently executing Action,\n * // which we reschedule with new state and delay\n * }\n *\n * asyncScheduler.schedule(task, 3000, 0);\n *\n * // Logs:\n * // 0 after 3s\n * // 1 after 4s\n * // 2 after 5s\n * // 3 after 6s\n * ```\n */\n\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\n\n/**\n * @deprecated Renamed to {@link asyncScheduler}. Will be removed in v8.\n */\nexport const async = asyncScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { Subscription } from '../Subscription';\nimport { QueueScheduler } from './QueueScheduler';\nimport { SchedulerAction } from '../types';\nimport { TimerHandle } from './timerHandle';\n\nexport class QueueAction extends AsyncAction {\n constructor(protected scheduler: QueueScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n public schedule(state?: T, delay: number = 0): Subscription {\n if (delay > 0) {\n return super.schedule(state, delay);\n }\n this.delay = delay;\n this.state = state;\n this.scheduler.flush(this);\n return this;\n }\n\n public execute(state: T, delay: number): any {\n return delay > 0 || this.closed ? super.execute(state, delay) : this._execute(state, delay);\n }\n\n protected requestAsyncId(scheduler: QueueScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n\n // Otherwise flush the scheduler starting with this action.\n scheduler.flush(this);\n\n // HACK: In the past, this was returning `void`. However, `void` isn't a valid\n // `TimerHandle`, and generally the return value here isn't really used. So the\n // compromise is to return `0` which is both \"falsy\" and a valid `TimerHandle`,\n // as opposed to refactoring every other instanceo of `requestAsyncId`.\n return 0;\n }\n}\n", "import { AsyncScheduler } from './AsyncScheduler';\n\nexport class QueueScheduler extends AsyncScheduler {\n}\n", "import { QueueAction } from './QueueAction';\nimport { QueueScheduler } from './QueueScheduler';\n\n/**\n *\n * Queue Scheduler\n *\n * Put every next task on a queue, instead of executing it immediately\n *\n * `queue` scheduler, when used with delay, behaves the same as {@link asyncScheduler} scheduler.\n *\n * When used without delay, it schedules given task synchronously - executes it right when\n * it is scheduled. However when called recursively, that is when inside the scheduled task,\n * another task is scheduled with queue scheduler, instead of executing immediately as well,\n * that task will be put on a queue and wait for current one to finish.\n *\n * This means that when you execute task with `queue` scheduler, you are sure it will end\n * before any other task scheduled with that scheduler will start.\n *\n * ## Examples\n * Schedule recursively first, then do something\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(() => {\n * queueScheduler.schedule(() => console.log('second')); // will not happen now, but will be put on a queue\n *\n * console.log('first');\n * });\n *\n * // Logs:\n * // \"first\"\n * // \"second\"\n * ```\n *\n * Reschedule itself recursively\n * ```ts\n * import { queueScheduler } from 'rxjs';\n *\n * queueScheduler.schedule(function(state) {\n * if (state !== 0) {\n * console.log('before', state);\n * this.schedule(state - 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * console.log('after', state);\n * }\n * }, 0, 3);\n *\n * // In scheduler that runs recursively, you would expect:\n * // \"before\", 3\n * // \"before\", 2\n * // \"before\", 1\n * // \"after\", 1\n * // \"after\", 2\n * // \"after\", 3\n *\n * // But with queue it logs:\n * // \"before\", 3\n * // \"after\", 3\n * // \"before\", 2\n * // \"after\", 2\n * // \"before\", 1\n * // \"after\", 1\n * ```\n */\n\nexport const queueScheduler = new QueueScheduler(QueueAction);\n\n/**\n * @deprecated Renamed to {@link queueScheduler}. Will be removed in v8.\n */\nexport const queue = queueScheduler;\n", "import { AsyncAction } from './AsyncAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nimport { SchedulerAction } from '../types';\nimport { animationFrameProvider } from './animationFrameProvider';\nimport { TimerHandle } from './timerHandle';\n\nexport class AnimationFrameAction extends AsyncAction {\n constructor(protected scheduler: AnimationFrameScheduler, protected work: (this: SchedulerAction, state?: T) => void) {\n super(scheduler, work);\n }\n\n protected requestAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle {\n // If delay is greater than 0, request as an async action.\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n // Push the action to the end of the scheduler queue.\n scheduler.actions.push(this);\n // If an animation frame has already been requested, don't request another\n // one. If an animation frame hasn't been requested yet, request one. Return\n // the current animation frame request id.\n return scheduler._scheduled || (scheduler._scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n\n protected recycleAsyncId(scheduler: AnimationFrameScheduler, id?: TimerHandle, delay: number = 0): TimerHandle | undefined {\n // If delay exists and is greater than 0, or if the delay is null (the\n // action wasn't rescheduled) but was originally scheduled as an async\n // action, then recycle as an async action.\n if (delay != null ? delay > 0 : this.delay > 0) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n // If the scheduler queue has no remaining actions with the same async id,\n // cancel the requested animation frame and set the scheduled flag to\n // undefined so the next AnimationFrameAction will request its own.\n const { actions } = scheduler;\n if (id != null && actions[actions.length - 1]?.id !== id) {\n animationFrameProvider.cancelAnimationFrame(id as number);\n scheduler._scheduled = undefined;\n }\n // Return undefined so the action knows to request a new async id if it's rescheduled.\n return undefined;\n }\n}\n", "import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\n\nexport class AnimationFrameScheduler extends AsyncScheduler {\n public flush(action?: AsyncAction): void {\n this._active = true;\n // The async id that effects a call to flush is stored in _scheduled.\n // Before executing an action, it's necessary to check the action's async\n // id to determine whether it's supposed to be executed in the current\n // flush.\n // Previous implementations of this method used a count to determine this,\n // but that was unsound, as actions that are unsubscribed - i.e. cancelled -\n // are removed from the actions array and that can shift actions that are\n // scheduled to be executed in a subsequent flush into positions at which\n // they are executed within the current flush.\n const flushId = this._scheduled;\n this._scheduled = undefined;\n\n const { actions } = this;\n let error: any;\n action = action || actions.shift()!;\n\n do {\n if ((error = action.execute(action.state, action.delay))) {\n break;\n }\n } while ((action = actions[0]) && action.id === flushId && actions.shift());\n\n this._active = false;\n\n if (error) {\n while ((action = actions[0]) && action.id === flushId && actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n", "import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\n\n/**\n *\n * Animation Frame Scheduler\n *\n * Perform task when `window.requestAnimationFrame` would fire\n *\n * When `animationFrame` scheduler is used with delay, it will fall back to {@link asyncScheduler} scheduler\n * behaviour.\n *\n * Without delay, `animationFrame` scheduler can be used to create smooth browser animations.\n * It makes sure scheduled task will happen just before next browser content repaint,\n * thus performing animations as efficiently as possible.\n *\n * ## Example\n * Schedule div height animation\n * ```ts\n * // html:
\n * import { animationFrameScheduler } from 'rxjs';\n *\n * const div = document.querySelector('div');\n *\n * animationFrameScheduler.schedule(function(height) {\n * div.style.height = height + \"px\";\n *\n * this.schedule(height + 1); // `this` references currently executing Action,\n * // which we reschedule with new state\n * }, 0, 0);\n *\n * // You will see a div element growing in height\n * ```\n */\n\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\n\n/**\n * @deprecated Renamed to {@link animationFrameScheduler}. Will be removed in v8.\n */\nexport const animationFrame = animationFrameScheduler;\n", "import { Observable } from '../Observable';\nimport { SchedulerLike } from '../types';\n\n/**\n * A simple Observable that emits no items to the Observer and immediately\n * emits a complete notification.\n *\n * Just emits 'complete', and nothing else.\n *\n * ![](empty.png)\n *\n * A simple Observable that only emits the complete notification. It can be used\n * for composing with other Observables, such as in a {@link mergeMap}.\n *\n * ## Examples\n *\n * Log complete notification\n *\n * ```ts\n * import { EMPTY } from 'rxjs';\n *\n * EMPTY.subscribe({\n * next: () => console.log('Next'),\n * complete: () => console.log('Complete!')\n * });\n *\n * // Outputs\n * // Complete!\n * ```\n *\n * Emit the number 7, then complete\n *\n * ```ts\n * import { EMPTY, startWith } from 'rxjs';\n *\n * const result = EMPTY.pipe(startWith(7));\n * result.subscribe(x => console.log(x));\n *\n * // Outputs\n * // 7\n * ```\n *\n * Map and flatten only odd numbers to the sequence `'a'`, `'b'`, `'c'`\n *\n * ```ts\n * import { interval, mergeMap, of, EMPTY } from 'rxjs';\n *\n * const interval$ = interval(1000);\n * const result = interval$.pipe(\n * mergeMap(x => x % 2 === 1 ? of('a', 'b', 'c') : EMPTY),\n * );\n * result.subscribe(x => console.log(x));\n *\n * // Results in the following to the console:\n * // x is equal to the count on the interval, e.g. (0, 1, 2, 3, ...)\n * // x will occur every 1000ms\n * // if x % 2 is equal to 1, print a, b, c (each on its own)\n * // if x % 2 is not equal to 1, nothing will be output\n * ```\n *\n * @see {@link Observable}\n * @see {@link NEVER}\n * @see {@link of}\n * @see {@link throwError}\n */\nexport const EMPTY = new Observable((subscriber) => subscriber.complete());\n\n/**\n * @param scheduler A {@link SchedulerLike} to use for scheduling\n * the emission of the complete notification.\n * @deprecated Replaced with the {@link EMPTY} constant or {@link scheduled} (e.g. `scheduled([], scheduler)`). Will be removed in v8.\n */\nexport function empty(scheduler?: SchedulerLike) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\n\nfunction emptyScheduled(scheduler: SchedulerLike) {\n return new Observable((subscriber) => scheduler.schedule(() => subscriber.complete()));\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport function isScheduler(value: any): value is SchedulerLike {\n return value && isFunction(value.schedule);\n}\n", "import { SchedulerLike } from '../types';\nimport { isFunction } from './isFunction';\nimport { isScheduler } from './isScheduler';\n\nfunction last(arr: T[]): T | undefined {\n return arr[arr.length - 1];\n}\n\nexport function popResultSelector(args: any[]): ((...args: unknown[]) => unknown) | undefined {\n return isFunction(last(args)) ? args.pop() : undefined;\n}\n\nexport function popScheduler(args: any[]): SchedulerLike | undefined {\n return isScheduler(last(args)) ? args.pop() : undefined;\n}\n\nexport function popNumber(args: any[], defaultValue: number): number {\n return typeof last(args) === 'number' ? args.pop()! : defaultValue;\n}\n", "export const isArrayLike = ((x: any): x is ArrayLike => x && typeof x.length === 'number' && typeof x !== 'function');", "import { isFunction } from \"./isFunction\";\n\n/**\n * Tests to see if the object is \"thennable\".\n * @param value the object to test\n */\nexport function isPromise(value: any): value is PromiseLike {\n return isFunction(value?.then);\n}\n", "import { InteropObservable } from '../types';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being Observable (but not necessary an Rx Observable) */\nexport function isInteropObservable(input: any): input is InteropObservable {\n return isFunction(input[Symbol_observable]);\n}\n", "import { isFunction } from './isFunction';\n\nexport function isAsyncIterable(obj: any): obj is AsyncIterable {\n return Symbol.asyncIterator && isFunction(obj?.[Symbol.asyncIterator]);\n}\n", "/**\n * Creates the TypeError to throw if an invalid object is passed to `from` or `scheduled`.\n * @param input The object that was passed.\n */\nexport function createInvalidObservableTypeError(input: any) {\n // TODO: We should create error codes that can be looked up, so this can be less verbose.\n return new TypeError(\n `You provided ${\n input !== null && typeof input === 'object' ? 'an invalid object' : `'${input}'`\n } where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.`\n );\n}\n", "export function getSymbolIterator(): symbol {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator' as any;\n }\n\n return Symbol.iterator;\n}\n\nexport const iterator = getSymbolIterator();\n", "import { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { isFunction } from './isFunction';\n\n/** Identifies an input as being an Iterable */\nexport function isIterable(input: any): input is Iterable {\n return isFunction(input?.[Symbol_iterator]);\n}\n", "import { ReadableStreamLike } from '../types';\nimport { isFunction } from './isFunction';\n\nexport async function* readableStreamLikeToAsyncGenerator(readableStream: ReadableStreamLike): AsyncGenerator {\n const reader = readableStream.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) {\n return;\n }\n yield value!;\n }\n } finally {\n reader.releaseLock();\n }\n}\n\nexport function isReadableStreamLike(obj: any): obj is ReadableStreamLike {\n // We don't want to use instanceof checks because they would return\n // false for instances from another Realm, like an + +
+

Try Mesop

+

If this sounds intriguing, read the Getting Started guide and try building your own Mesop app. Share your feedback and contribute as we continue developing Mesop.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/index.html b/blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/index.html new file mode 100644 index 000000000..07de57100 --- /dev/null +++ b/blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/index.html @@ -0,0 +1,1265 @@ + + + + + + + + + + + + + + + + + + + + + Is Mesop + Web Components the cure to Front-end fatigue? - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + + + + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+
+ + +
+
+
+
+ + + + + + + +

Is Mesop + Web Components the cure to Front-end fatigue?

+

I saw this tweet the other day and couldn't help but chuckle:

+ + + +

At first, I thought of it as joke, but now that Mesop has launched experimental support for Web Components, I think it's plausible that Mesop with Web Components can save you from front-end fatigue.

+

What is Mesop?

+

Before we dive in, let me explain what Mesop is. Mesop is a Python UI framework focused on rapidly building AI apps. You can write a lot of kinds of apps all in Python as you can see from the examples on our home page, but when you need to, Mesop provides the flexibility of dropping down into web components so you can have fine-grained UI control and use existing JS libraries.

+

Avoid the builds

+

Programming meme

+

DHH, creator of Rails, recently gave an interview saying how he's "done with bundling" and the overall complexity of modern front-end build toolchains.

+

As someone who's done front-end for almost a decade, I can attest to the sentiment of feeling the pain of compiling JavaScript options. Setting up compiler configs and options can easily take hours. I want to be clear, I think a lot of these tools like TypeScript are fantastic, and the core Mesop framework itself is compiled using TypeScript and Angular's compilers.

+

But when it comes to rapid prototyping, I want to avoid that overhead. In our design proposal, we intentionally designed a lightweight model where you don't need to set up a complex build chain to start writing JavaScript.

+

Sometimes a build step is unavoidable, e.g. you're writing TypeScript, and you can still compile your JavaScript as needed.

+

Framework churn

+

The front-end ecosystem is infamous for its steady and constant churn. The good thing about building on top of web components is that it's based on web standards supported by all modern browsers. This means, that given browser makers' focus on "not breaking the web", this will be there for many years, if not decades to come.

+

For years, web components had a reputation of being an immature technology due to inconsistent support across browsers, but fast forward to 2024, and web components are well-supported in modern browsers and libraries built on web components like Lit, which is downloaded millions of times a week.

+

Minimizing front-end fatigue in Mesop

+

FE developers are so used to the pain and complexity of front-end development that they can forget how steep the learning curve is until someone from another domain tries to build a simple web app, and struggles with just getting the web app up and started.

+

Mesop app developers are mostly not front-end developers which means that reducing the complexity, especially learning curve, of building custom components is very important. In Mesop, we've designed a smooth pathway where you can get started with a little front-end knowledge and build simple custom components without learning a complex front-end framework.

+

What's next

+

Follow our X/Twitter account, @mesop_dev for more updates. We're working on improving our web component support, in particular by:

+
    +
  • Creating guides for wrapping React components into Mesop web components
  • +
  • Fostering an ecosystem of open-source Mesop web components by making it easy to discover and reuse web components that other people have built.
  • +
+

We're excited about the potential of Mesop and Web Components to simplify front-end development. Whether it's the cure for front-end fatigue remains to be seen, but I think it offers a promising alternative to the complexity of traditional front-end development.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/blog/archive/2023/index.html b/blog/archive/2023/index.html new file mode 100644 index 000000000..0638cd639 --- /dev/null +++ b/blog/archive/2023/index.html @@ -0,0 +1,1213 @@ + + + + + + + + + + + + + + + + + + + + + + + 2023 - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

2023

+
+ +
+
+ + +
+
+

Hello, Mesop

+

After working on Mesop for the last two months, I'm excited to finally announce the first version of Mesop, v0.1. This is still early days for Mesop, but it's an important milestone because it represents a minimum viable tool for building UIs in Python. In case you haven't read Mesop's home page, Mesop is a Python-based UI framework that allows you to rapidly build web demos. Engineers without frontend experience can build web UIs by writing idiomatic Python code.

+

Why Mesop?

+

Mesop is in many ways a remix of many existing ideas packaged into a single cohesive UI framework, designed for Python developers. I've documented some of these goals previously, but I'll quickly recap the benefits of Mesop here:

+
    +
  • Allows non-frontend engineers to rapidly build UIs for internal use cases like demos.
  • +
  • Provides a fast build-edit-refresh loop through hot reload.
  • +
  • Enables developers to benefit from the mature Angular web framework and Angular Material components.
  • +
  • Provides a flexible and composable components API that's idiomatic to Python.
  • +
  • Easy to deploy by using standard HTTP technologies like Server-Sent Events.
  • +
+

What's next for Mesop?

+

I see a few broad themes of work in the coming year or so.

+

Expand Mesop's component library

+

Mesop's current component library is a solid start but there's still gaps to support common use cases.

+

Areas of work:

+
    +
  • +

    Complete Angular Material component coverage. We support 13+ Angular Material components today, however there's many more that we don't support. Some of it is because I haven't had time to wrap their components, but in other cases (e.g. sidenav), I'd like to spend more time exploring the design space as it will probably require supporting some kind of multi-slot component API. Getting this API designed correctly, for not just this component but also future components, is important in the long run.

    +
  • +
  • +

    Support more native HTML elements/browser APIs. Right now, only Box and Text are thin wrappers around native HTML elements. However, there are other HTML elements like <img>, <audio> + and <video> that I'd like to also support. The flip side of supporting these components is enabling a way to allow Mesop end-users to upload these media contents, which there are also native browser APIs for.

    +
  • +
  • +

    Custom components. Some components won't belong in the standard Mesop package because it's either too experimental or too use-case specific. It would be nice to have a complete story for supporting custom components. Today, all of the components use the component helper API which wraps internal framework details like runtime. However, there still isn't a very good story for loading custom components in the Angular frontend (e.g. ComponentRenderer's type to component map) and testing them.

    +
  • +
+

Make it easy to get started with Mesop

+

Using Mesop today requires following our internal development setup which requires dependencies like Bazel/iBazel which makes it easy to interoperate with our downstream sync, but these dependencies aren't commonly used in the Python ecosystem. Eventually, I'd like make using Mesop as simple as pip install mesop and then using Mesop's built-in CLI: mesop serve for local development and mesop deploy to deploy on a Cloud service.

+

Areas of work:

+
    +
  • +

    Find a suitable ibazel replacement for Hot Reload. Instead of requiring Mesop developers to sync the entire repo and building the project with Bazel and iBazel, we should distribute a ready-to-use pip package of Mesop. However, this leaves an open question of how we support hot reload without iBazel which provides: 1) a filesystem watching mechanism and 2) live reload. We'll need to investigate good open-source equivalents for each of these capabilities.

    +
  • +
  • +

    Provide web-based interactive demos. Many JavaScript UI frameworks provide a playground (e.g. Angular) or interactive tutorial (e.g. Solid) so that prospective developers can use the framework before going through the hassle of setting up their own local dev environment. This would also be very helpful to provide for each component as it's a lot easier to understand a component by tinkering with a live example.

    +
  • +
+

Explore power use cases

+

Today Mesop is good for internal apps with relatively un-stringent demands in terms of UI customizability and performance. For production-grade external apps, there's several areas that Mesop would need to advance in, before it's ready.

+

Areas of work:

+
    +
  • +

    Optimize network payload. Right now the client sends the entire state to the server, and the server responds with the entire state and component tree. For large UIs/apps, this can result in sizable network payloads. We can optimize this by sending deltas as much as possible. For example, the server can send a delta of the state and component tree to the client. In addition, if we use POST instead of GET, we can stop using base-64 encoding which adds a significant overhead on top of Protobuf binary serialization.

    +
  • +
  • +

    Stateful server. Even with the above optimizations, we'd essentially preserve the current architecture, but there's some limitations in how much improvements we can make as long as we assume servers are stateless. However, if we allow stateful servers (i.e. long-lived connections between the client and server), we can use things like WebSockets and always send deltas bi-directionally, in particular from client to server which isn't possible with a stateless server. The problem with this direction, though, is that it makes deployment more complex as scaling a WebSocket-based server can be hard depending on the cloud infrastructure used. In addition, we'll need to handle new edge cases like authentication and broken WebSockets connections.

    +
  • +
  • +

    Optimistic UI. One of the drawbacks for server-driven UI frameworks like Mesop is that it introduces significant latency to simple user interactions. For example, if you click a button, it requires a network roundtrip before the UI is (meaningfully) updated. One way of dealing with this shortcoming is by pre-fetching the next UI state based on a user hint. For example, if a user is hovering over a button, we could optimistically calculate the state change and component tree change ahead of time before the actual click. The obvious downside to this is that optimistically executing an action is inappropriate in many cases, for example, a non-reversible action (e.g. delete) should never be optimistically done. To safely introduce this concept, we could provide an (optional) annotation for event handlers like @me.optimistic(events=[me.HoverEvent]) so develpers could opt-in.

    +
  • +
+

Some of these directions are potentially mutually exclusive. For example, having a stateful server may make optimistic UI practically more difficult because a stateful server means that non-serializable state could start to creep in to Mesop applications which makes undoing optimistic UI updates tricky

+

There's, of course, even more directions than what I've listed here. For example, it's technically possible to compile Python into WebAssembly and run it in the browser and this could be another way of tackling latency to user interactions. However, this seems like a longer-term exploration, which is why I've left it out for now.

+

Interested in contributing?

+

If any of this excites you, please reach out. The easiest way is to raise a GitHub issue and let me know if there's something specific you'd like to contribute.

+ +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/blog/archive/2024/index.html b/blog/archive/2024/index.html new file mode 100644 index 000000000..e2a3f077a --- /dev/null +++ b/blog/archive/2024/index.html @@ -0,0 +1,1312 @@ + + + + + + + + + + + + + + + + + + + + + + + 2024 - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

2024

+
+ +
+
+ + +
+
+

Is Mesop + Web Components the cure to Front-end fatigue?

+

I saw this tweet the other day and couldn't help but chuckle:

+ + + +

At first, I thought of it as joke, but now that Mesop has launched experimental support for Web Components, I think it's plausible that Mesop with Web Components can save you from front-end fatigue.

+

What is Mesop?

+

Before we dive in, let me explain what Mesop is. Mesop is a Python UI framework focused on rapidly building AI apps. You can write a lot of kinds of apps all in Python as you can see from the examples on our home page, but when you need to, Mesop provides the flexibility of dropping down into web components so you can have fine-grained UI control and use existing JS libraries.

+

Avoid the builds

+

Programming meme

+

DHH, creator of Rails, recently gave an interview saying how he's "done with bundling" and the overall complexity of modern front-end build toolchains.

+

As someone who's done front-end for almost a decade, I can attest to the sentiment of feeling the pain of compiling JavaScript options. Setting up compiler configs and options can easily take hours. I want to be clear, I think a lot of these tools like TypeScript are fantastic, and the core Mesop framework itself is compiled using TypeScript and Angular's compilers.

+

But when it comes to rapid prototyping, I want to avoid that overhead. In our design proposal, we intentionally designed a lightweight model where you don't need to set up a complex build chain to start writing JavaScript.

+

Sometimes a build step is unavoidable, e.g. you're writing TypeScript, and you can still compile your JavaScript as needed.

+

Framework churn

+

The front-end ecosystem is infamous for its steady and constant churn. The good thing about building on top of web components is that it's based on web standards supported by all modern browsers. This means, that given browser makers' focus on "not breaking the web", this will be there for many years, if not decades to come.

+

For years, web components had a reputation of being an immature technology due to inconsistent support across browsers, but fast forward to 2024, and web components are well-supported in modern browsers and libraries built on web components like Lit, which is downloaded millions of times a week.

+

Minimizing front-end fatigue in Mesop

+

FE developers are so used to the pain and complexity of front-end development that they can forget how steep the learning curve is until someone from another domain tries to build a simple web app, and struggles with just getting the web app up and started.

+

Mesop app developers are mostly not front-end developers which means that reducing the complexity, especially learning curve, of building custom components is very important. In Mesop, we've designed a smooth pathway where you can get started with a little front-end knowledge and build simple custom components without learning a complex front-end framework.

+

What's next

+

Follow our X/Twitter account, @mesop_dev for more updates. We're working on improving our web component support, in particular by:

+
    +
  • Creating guides for wrapping React components into Mesop web components
  • +
  • Fostering an ecosystem of open-source Mesop web components by making it easy to discover and reuse web components that other people have built.
  • +
+

We're excited about the potential of Mesop and Web Components to simplify front-end development. Whether it's the cure for front-end fatigue remains to be seen, but I think it offers a promising alternative to the complexity of traditional front-end development.

+ +
+
+ +
+
+ + +
+
+

Why Mesop?

+

Mesop is a new UI framework that enables Python developers to quickly build delightful web apps in a scalable way.

+

Many Python UI frameworks are easy to get started with, but customizing beyond the defaults often requires diving into JavaScript, CSS, and HTML — a steep learning curve for many developers.

+

Mesop provides a different approach, offering a framework that's both easy to learn and enables flexible UI building, all within Python.

+

I want to share a couple concrete ways in which Mesop achieves this.

+

Build UIs with Functions (i.e. Components)

+

Mesop embraces a component-based philosophy where the entire UI is composed of reusable, building blocks which are called components. Using a component is as simple as calling a Python function. This approach offers several benefits:

+
    +
  • Simplicity: You can use your existing Python knowledge to build UIs quickly and intuitively since components are just functions.
  • +
  • Maintainability: Complex UIs become easier to manage and understand by breaking them down into smaller, focused components.
  • +
  • Modularity: Components are self-contained, enabling easy reuse within a project or across different projects.
  • +
+

Here's an example of a reusable icon button component:

+
def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):
+  """Icon button with text and tooltip."""
+  with me.content_button(on_click=on_click):
+    with me.tooltip(message=tooltip):
+      with me.box(style=me.Style(display="flex")):
+        me.icon(icon=icon)
+        me.text(
+          label, style=me.Style(line_height="24px", margin=me.Margin(left=5))
+        )
+
+

Flexibility through Layered Building Blocks

+

Mesop provides a range of UI building blocks, from low-level native components to high-level components.

+
    +
  • Low-level components: like box, offer granular control over layout and styling. They empower you to create custom UI elements through flexible layouts like flexbox and grid.
  • +
  • High-level components: like chat, are built from low-level components and provide ready-to-use elements for common use cases, enabling rapid development.
  • +
+

This layered approach makes deep customization possible. This means that if you want to customize the chat component, you can fork the chat implementation because it's written entirely in Python using Mesop's public APIs.

+

See Mesop in Action

+

To demonstrate the range of UIs possible with Mesop, we built a demo gallery to showcase the types of applications you can build and the components that are available:

+ + +

The demo gallery itself is a Mesop app and implemented in a few hundred lines of Python code. It demonstrates how Mesop can be used to create polished, custom UIs in a maintainable way.

+

Try Mesop

+

If this sounds intriguing, read the Getting Started guide and try building your own Mesop app. Share your feedback and contribute as we continue developing Mesop.

+ +
+
+ +
+
+ + +
+
+

Visual Editor

+

Why?

+

As I began discussing Mesop with friends and colleagues, one thing that has come up is the difficulty of teaching and persuading non-frontend engineers to build UIs, even simple ones. CSS, particularly the rules around layout, can be quite challenging and off-putting.

+

I've developed a new visual editor for Mesop that aims to make UI building more approachable for beginners and more productive for experts.

+

What?

+

Let's take a look at the visual editor:

+

Visual Editor v1

+

With the visual editor, you can:

+
    +
  • Add new components into your app
  • +
  • Modify existing components
  • +
  • Visualize the component tree hierarchy
  • +
  • You can inspect existing components on the page by hovering over them and then change them in the editor panel
  • +
  • Bring Your Own components. By decorating a Python function with me.component, you've turned it into a Mesop component and you can now add it with the visual editor.
  • +
+

What's exciting about the visual editor is that you aren't locked into it - everytime you change a component with the visual editor, it's modifying the source code directly so you can seamlessly go back forth between a regular text editor and the visual editor to build your Mesop app.

+

Prior Art

+

Visual editors (aka WYSIWYG builders) have been around for a long time. Puck is one of the most interesting ones because of a few reasons: 1) it's open-source, 2) it's flexible (e.g. bring your own components) and 3) it's intuitive and easy-to-use.

+

The main issues I saw with Puck, particularly for Mesop's use case, is that it currently only supports React (and Mesop uses Angular) and Puck saves data whereas I would like Mesop's Visual Editor to directly emit/update code, which I'll explain next.

+

Principles

+

Hybrid code (not low-code)

+

One of the reasons why WYSIWYG builders have not gotten much traction with engineers is that they're often good for simple applications, but then you hit a wall building more complex applications.

+

To avoid this issue, I'm focusing on making the Visual Editor actually emit code and not just data. Essentially, the UI code that you produce from the Visual Editor should be the same as the code that you would write by hand.

+

Unobtrustive UI

+

I want Mesop app developers to do most of their work (except for the final finetuning for deployment) in the Visual Editior which means that it's important the Editor UI is un-obtrusive. Chrome DevTools is a great example of a low-key tool that many web developers keep open throughout their development - it's helpful for debugging, but then it's out of your way as you're interacting with the application.

+

Concretely, this means:

+
    +
  • Editor UI should be collapsible
  • +
  • You should be able to "disable" the editor mode and interact with the application as a normal user.
  • +
+

Contextual

+

The visual editor should provide only the information that you need when you need it.

+

For example, rather than showing all the style properties in the editor panel, which would be quite overwhelming, we only show the style properties that you're using for the selected component.

+

Local-only

+

Because the Visual Editor relies on editing files in your local filesystem, I want to avoid any accidental usages out in the wild. Concretely, this means that you can only use the Visual Editor in localhost, otherwise the Mesop server will reject the editor edit requests.

+

What's next

+

There's still a lot of improvements and polishes I would like to make to the visual editor, but a few high-level ideas that I have are:

+
    +
  1. Build example applications using the visual editor with a video walkthrough.
  2. +
  3. Create more high-level components in Mesop Labs, which I'll introduce in an upcoming blog post, to make it even easier to build apps with the visual editor.
  4. +
  5. Drag and drop components onto the page and within the page. This will provide an intuitive experience for building the UI, literally block by block.
  6. +
+ +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/blog/index.html b/blog/index.html new file mode 100644 index 000000000..bd1ea4906 --- /dev/null +++ b/blog/index.html @@ -0,0 +1,1392 @@ + + + + + + + + + + + + + + + + + + + + + + + Blog Home - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+
+

Blog Home

+
+ +
+
+ + +
+
+

Is Mesop + Web Components the cure to Front-end fatigue?

+

I saw this tweet the other day and couldn't help but chuckle:

+ + + +

At first, I thought of it as joke, but now that Mesop has launched experimental support for Web Components, I think it's plausible that Mesop with Web Components can save you from front-end fatigue.

+

What is Mesop?

+

Before we dive in, let me explain what Mesop is. Mesop is a Python UI framework focused on rapidly building AI apps. You can write a lot of kinds of apps all in Python as you can see from the examples on our home page, but when you need to, Mesop provides the flexibility of dropping down into web components so you can have fine-grained UI control and use existing JS libraries.

+

Avoid the builds

+

Programming meme

+

DHH, creator of Rails, recently gave an interview saying how he's "done with bundling" and the overall complexity of modern front-end build toolchains.

+

As someone who's done front-end for almost a decade, I can attest to the sentiment of feeling the pain of compiling JavaScript options. Setting up compiler configs and options can easily take hours. I want to be clear, I think a lot of these tools like TypeScript are fantastic, and the core Mesop framework itself is compiled using TypeScript and Angular's compilers.

+

But when it comes to rapid prototyping, I want to avoid that overhead. In our design proposal, we intentionally designed a lightweight model where you don't need to set up a complex build chain to start writing JavaScript.

+

Sometimes a build step is unavoidable, e.g. you're writing TypeScript, and you can still compile your JavaScript as needed.

+

Framework churn

+

The front-end ecosystem is infamous for its steady and constant churn. The good thing about building on top of web components is that it's based on web standards supported by all modern browsers. This means, that given browser makers' focus on "not breaking the web", this will be there for many years, if not decades to come.

+

For years, web components had a reputation of being an immature technology due to inconsistent support across browsers, but fast forward to 2024, and web components are well-supported in modern browsers and libraries built on web components like Lit, which is downloaded millions of times a week.

+

Minimizing front-end fatigue in Mesop

+

FE developers are so used to the pain and complexity of front-end development that they can forget how steep the learning curve is until someone from another domain tries to build a simple web app, and struggles with just getting the web app up and started.

+

Mesop app developers are mostly not front-end developers which means that reducing the complexity, especially learning curve, of building custom components is very important. In Mesop, we've designed a smooth pathway where you can get started with a little front-end knowledge and build simple custom components without learning a complex front-end framework.

+

What's next

+

Follow our X/Twitter account, @mesop_dev for more updates. We're working on improving our web component support, in particular by:

+
    +
  • Creating guides for wrapping React components into Mesop web components
  • +
  • Fostering an ecosystem of open-source Mesop web components by making it easy to discover and reuse web components that other people have built.
  • +
+

We're excited about the potential of Mesop and Web Components to simplify front-end development. Whether it's the cure for front-end fatigue remains to be seen, but I think it offers a promising alternative to the complexity of traditional front-end development.

+ +
+
+ +
+
+ + +
+
+

Why Mesop?

+

Mesop is a new UI framework that enables Python developers to quickly build delightful web apps in a scalable way.

+

Many Python UI frameworks are easy to get started with, but customizing beyond the defaults often requires diving into JavaScript, CSS, and HTML — a steep learning curve for many developers.

+

Mesop provides a different approach, offering a framework that's both easy to learn and enables flexible UI building, all within Python.

+

I want to share a couple concrete ways in which Mesop achieves this.

+

Build UIs with Functions (i.e. Components)

+

Mesop embraces a component-based philosophy where the entire UI is composed of reusable, building blocks which are called components. Using a component is as simple as calling a Python function. This approach offers several benefits:

+
    +
  • Simplicity: You can use your existing Python knowledge to build UIs quickly and intuitively since components are just functions.
  • +
  • Maintainability: Complex UIs become easier to manage and understand by breaking them down into smaller, focused components.
  • +
  • Modularity: Components are self-contained, enabling easy reuse within a project or across different projects.
  • +
+

Here's an example of a reusable icon button component:

+
def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):
+  """Icon button with text and tooltip."""
+  with me.content_button(on_click=on_click):
+    with me.tooltip(message=tooltip):
+      with me.box(style=me.Style(display="flex")):
+        me.icon(icon=icon)
+        me.text(
+          label, style=me.Style(line_height="24px", margin=me.Margin(left=5))
+        )
+
+

Flexibility through Layered Building Blocks

+

Mesop provides a range of UI building blocks, from low-level native components to high-level components.

+
    +
  • Low-level components: like box, offer granular control over layout and styling. They empower you to create custom UI elements through flexible layouts like flexbox and grid.
  • +
  • High-level components: like chat, are built from low-level components and provide ready-to-use elements for common use cases, enabling rapid development.
  • +
+

This layered approach makes deep customization possible. This means that if you want to customize the chat component, you can fork the chat implementation because it's written entirely in Python using Mesop's public APIs.

+

See Mesop in Action

+

To demonstrate the range of UIs possible with Mesop, we built a demo gallery to showcase the types of applications you can build and the components that are available:

+ + +

The demo gallery itself is a Mesop app and implemented in a few hundred lines of Python code. It demonstrates how Mesop can be used to create polished, custom UIs in a maintainable way.

+

Try Mesop

+

If this sounds intriguing, read the Getting Started guide and try building your own Mesop app. Share your feedback and contribute as we continue developing Mesop.

+ +
+
+ +
+
+ + +
+
+

Visual Editor

+

Why?

+

As I began discussing Mesop with friends and colleagues, one thing that has come up is the difficulty of teaching and persuading non-frontend engineers to build UIs, even simple ones. CSS, particularly the rules around layout, can be quite challenging and off-putting.

+

I've developed a new visual editor for Mesop that aims to make UI building more approachable for beginners and more productive for experts.

+

What?

+

Let's take a look at the visual editor:

+

Visual Editor v1

+

With the visual editor, you can:

+
    +
  • Add new components into your app
  • +
  • Modify existing components
  • +
  • Visualize the component tree hierarchy
  • +
  • You can inspect existing components on the page by hovering over them and then change them in the editor panel
  • +
  • Bring Your Own components. By decorating a Python function with me.component, you've turned it into a Mesop component and you can now add it with the visual editor.
  • +
+

What's exciting about the visual editor is that you aren't locked into it - everytime you change a component with the visual editor, it's modifying the source code directly so you can seamlessly go back forth between a regular text editor and the visual editor to build your Mesop app.

+

Prior Art

+

Visual editors (aka WYSIWYG builders) have been around for a long time. Puck is one of the most interesting ones because of a few reasons: 1) it's open-source, 2) it's flexible (e.g. bring your own components) and 3) it's intuitive and easy-to-use.

+

The main issues I saw with Puck, particularly for Mesop's use case, is that it currently only supports React (and Mesop uses Angular) and Puck saves data whereas I would like Mesop's Visual Editor to directly emit/update code, which I'll explain next.

+

Principles

+

Hybrid code (not low-code)

+

One of the reasons why WYSIWYG builders have not gotten much traction with engineers is that they're often good for simple applications, but then you hit a wall building more complex applications.

+

To avoid this issue, I'm focusing on making the Visual Editor actually emit code and not just data. Essentially, the UI code that you produce from the Visual Editor should be the same as the code that you would write by hand.

+

Unobtrustive UI

+

I want Mesop app developers to do most of their work (except for the final finetuning for deployment) in the Visual Editior which means that it's important the Editor UI is un-obtrusive. Chrome DevTools is a great example of a low-key tool that many web developers keep open throughout their development - it's helpful for debugging, but then it's out of your way as you're interacting with the application.

+

Concretely, this means:

+
    +
  • Editor UI should be collapsible
  • +
  • You should be able to "disable" the editor mode and interact with the application as a normal user.
  • +
+

Contextual

+

The visual editor should provide only the information that you need when you need it.

+

For example, rather than showing all the style properties in the editor panel, which would be quite overwhelming, we only show the style properties that you're using for the selected component.

+

Local-only

+

Because the Visual Editor relies on editing files in your local filesystem, I want to avoid any accidental usages out in the wild. Concretely, this means that you can only use the Visual Editor in localhost, otherwise the Mesop server will reject the editor edit requests.

+

What's next

+

There's still a lot of improvements and polishes I would like to make to the visual editor, but a few high-level ideas that I have are:

+
    +
  1. Build example applications using the visual editor with a video walkthrough.
  2. +
  3. Create more high-level components in Mesop Labs, which I'll introduce in an upcoming blog post, to make it even easier to build apps with the visual editor.
  4. +
  5. Drag and drop components onto the page and within the page. This will provide an intuitive experience for building the UI, literally block by block.
  6. +
+ +
+
+ +
+
+ + +
+
+

Hello, Mesop

+

After working on Mesop for the last two months, I'm excited to finally announce the first version of Mesop, v0.1. This is still early days for Mesop, but it's an important milestone because it represents a minimum viable tool for building UIs in Python. In case you haven't read Mesop's home page, Mesop is a Python-based UI framework that allows you to rapidly build web demos. Engineers without frontend experience can build web UIs by writing idiomatic Python code.

+

Why Mesop?

+

Mesop is in many ways a remix of many existing ideas packaged into a single cohesive UI framework, designed for Python developers. I've documented some of these goals previously, but I'll quickly recap the benefits of Mesop here:

+
    +
  • Allows non-frontend engineers to rapidly build UIs for internal use cases like demos.
  • +
  • Provides a fast build-edit-refresh loop through hot reload.
  • +
  • Enables developers to benefit from the mature Angular web framework and Angular Material components.
  • +
  • Provides a flexible and composable components API that's idiomatic to Python.
  • +
  • Easy to deploy by using standard HTTP technologies like Server-Sent Events.
  • +
+

What's next for Mesop?

+

I see a few broad themes of work in the coming year or so.

+

Expand Mesop's component library

+

Mesop's current component library is a solid start but there's still gaps to support common use cases.

+

Areas of work:

+
    +
  • +

    Complete Angular Material component coverage. We support 13+ Angular Material components today, however there's many more that we don't support. Some of it is because I haven't had time to wrap their components, but in other cases (e.g. sidenav), I'd like to spend more time exploring the design space as it will probably require supporting some kind of multi-slot component API. Getting this API designed correctly, for not just this component but also future components, is important in the long run.

    +
  • +
  • +

    Support more native HTML elements/browser APIs. Right now, only Box and Text are thin wrappers around native HTML elements. However, there are other HTML elements like <img>, <audio> + and <video> that I'd like to also support. The flip side of supporting these components is enabling a way to allow Mesop end-users to upload these media contents, which there are also native browser APIs for.

    +
  • +
  • +

    Custom components. Some components won't belong in the standard Mesop package because it's either too experimental or too use-case specific. It would be nice to have a complete story for supporting custom components. Today, all of the components use the component helper API which wraps internal framework details like runtime. However, there still isn't a very good story for loading custom components in the Angular frontend (e.g. ComponentRenderer's type to component map) and testing them.

    +
  • +
+

Make it easy to get started with Mesop

+

Using Mesop today requires following our internal development setup which requires dependencies like Bazel/iBazel which makes it easy to interoperate with our downstream sync, but these dependencies aren't commonly used in the Python ecosystem. Eventually, I'd like make using Mesop as simple as pip install mesop and then using Mesop's built-in CLI: mesop serve for local development and mesop deploy to deploy on a Cloud service.

+

Areas of work:

+
    +
  • +

    Find a suitable ibazel replacement for Hot Reload. Instead of requiring Mesop developers to sync the entire repo and building the project with Bazel and iBazel, we should distribute a ready-to-use pip package of Mesop. However, this leaves an open question of how we support hot reload without iBazel which provides: 1) a filesystem watching mechanism and 2) live reload. We'll need to investigate good open-source equivalents for each of these capabilities.

    +
  • +
  • +

    Provide web-based interactive demos. Many JavaScript UI frameworks provide a playground (e.g. Angular) or interactive tutorial (e.g. Solid) so that prospective developers can use the framework before going through the hassle of setting up their own local dev environment. This would also be very helpful to provide for each component as it's a lot easier to understand a component by tinkering with a live example.

    +
  • +
+

Explore power use cases

+

Today Mesop is good for internal apps with relatively un-stringent demands in terms of UI customizability and performance. For production-grade external apps, there's several areas that Mesop would need to advance in, before it's ready.

+

Areas of work:

+
    +
  • +

    Optimize network payload. Right now the client sends the entire state to the server, and the server responds with the entire state and component tree. For large UIs/apps, this can result in sizable network payloads. We can optimize this by sending deltas as much as possible. For example, the server can send a delta of the state and component tree to the client. In addition, if we use POST instead of GET, we can stop using base-64 encoding which adds a significant overhead on top of Protobuf binary serialization.

    +
  • +
  • +

    Stateful server. Even with the above optimizations, we'd essentially preserve the current architecture, but there's some limitations in how much improvements we can make as long as we assume servers are stateless. However, if we allow stateful servers (i.e. long-lived connections between the client and server), we can use things like WebSockets and always send deltas bi-directionally, in particular from client to server which isn't possible with a stateless server. The problem with this direction, though, is that it makes deployment more complex as scaling a WebSocket-based server can be hard depending on the cloud infrastructure used. In addition, we'll need to handle new edge cases like authentication and broken WebSockets connections.

    +
  • +
  • +

    Optimistic UI. One of the drawbacks for server-driven UI frameworks like Mesop is that it introduces significant latency to simple user interactions. For example, if you click a button, it requires a network roundtrip before the UI is (meaningfully) updated. One way of dealing with this shortcoming is by pre-fetching the next UI state based on a user hint. For example, if a user is hovering over a button, we could optimistically calculate the state change and component tree change ahead of time before the actual click. The obvious downside to this is that optimistically executing an action is inappropriate in many cases, for example, a non-reversible action (e.g. delete) should never be optimistically done. To safely introduce this concept, we could provide an (optional) annotation for event handlers like @me.optimistic(events=[me.HoverEvent]) so develpers could opt-in.

    +
  • +
+

Some of these directions are potentially mutually exclusive. For example, having a stateful server may make optimistic UI practically more difficult because a stateful server means that non-serializable state could start to creep in to Mesop applications which makes undoing optimistic UI updates tricky

+

There's, of course, even more directions than what I've listed here. For example, it's technically possible to compile Python into WebAssembly and run it in the browser and this could be another way of tackling latency to user interactions. However, this seems like a longer-term exploration, which is why I've left it out for now.

+

Interested in contributing?

+

If any of this excites you, please reach out. The easiest way is to raise a GitHub issue and let me know if there's something specific you'd like to contribute.

+ +
+
+ + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/codelab/2/index.html b/codelab/2/index.html new file mode 100644 index 000000000..73619635e --- /dev/null +++ b/codelab/2/index.html @@ -0,0 +1,1580 @@ + + + + + + + + + + + + + + + + + + + + + + + 2 - Building the basic UI - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DuoChat Codelab Part 2: Building the basic UI

+

In this section, we'll create the main layout for our DuoChat application, including the header, chat input area, and some basic styling. We'll use Mesop's components and styling system to create an attractive and functional UI.

+

Updating the Main Layout

+

Let's start by updating our main.py file to include a more structured layout. We'll use Mesop's box component for layout and add some custom styles.

+

Replace the content of main.py with the following:

+
main.py
import mesop as me
+
+ROOT_BOX_STYLE = me.Style(
+    background="#e7f2ff",
+    height="100%",
+    font_family="Inter",
+    display="flex",
+    flex_direction="column",
+)
+
+@me.page(
+    path="/",
+    stylesheets=[
+        "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
+    ],
+)
+def page():
+    with me.box(style=ROOT_BOX_STYLE):
+        header()
+        with me.box(
+            style=me.Style(
+                width="min(680px, 100%)",
+                margin=me.Margin.symmetric(
+                    horizontal="auto",
+                    vertical=36,
+                ),
+            )
+        ):
+            me.text(
+                "Chat with multiple models at once",
+                style=me.Style(
+                    font_size=20,
+                    margin=me.Margin(bottom=24),
+                ),
+            )
+            chat_input()
+
+def header():
+    with me.box(
+        style=me.Style(
+            padding=me.Padding.all(16),
+        ),
+    ):
+        me.text(
+            "DuoChat",
+            style=me.Style(
+                font_weight=500,
+                font_size=24,
+                color="#3D3929",
+                letter_spacing="0.3px",
+            ),
+        )
+
+def chat_input():
+    with me.box(
+        style=me.Style(
+            border_radius=16,
+            padding=me.Padding.all(8),
+            background="white",
+            display="flex",
+            width="100%",
+        )
+    ):
+        with me.box(style=me.Style(flex_grow=1)):
+            me.native_textarea(
+                placeholder="Enter a prompt",
+                style=me.Style(
+                    padding=me.Padding(top=16, left=16),
+                    outline="none",
+                    width="100%",
+                    border=me.Border.all(me.BorderSide(style="none")),
+                ),
+            )
+        with me.content_button(type="icon"):
+            me.icon("send")
+
+

Run the Mesop app and look at the changes:

+
mesop main.py
+
+

Let's review the changes:

+
    +
  1. We've added a ROOT_BOX_STYLE to set the overall layout and background color.
  2. +
  3. We're importing a custom font (Inter) using the stylesheets parameter in the @me.page decorator.
  4. +
  5. We've created separate functions for the header and chat_input components.
  6. +
  7. The main layout uses nested box components with custom styles to create a centered, responsive design.
  8. +
+

Understanding Mesop's Styling System

+

Mesop's styling system is based on Python classes that correspond to CSS properties. You can learn more by reading the Style API docs.

+

Adding Interactivity

+

Now, let's add some basic interactivity to our chat input. We'll update the chat_input function to handle user input:

+
main.py
@me.stateclass
+class State:
+    input: str = ""
+
+def on_blur(e: me.InputBlurEvent):
+    state = me.state(State)
+    state.input = e.value
+
+def chat_input():
+    state = me.state(State)
+    with me.box(
+        style=me.Style(
+            border_radius=16,
+            padding=me.Padding.all(8),
+            background="white",
+            display="flex",
+            width="100%",
+        )
+    ):
+        with me.box(style=me.Style(flex_grow=1)):
+            me.native_textarea(
+                value=state.input,
+                placeholder="Enter a prompt",
+                on_blur=on_blur,
+                style=me.Style(
+                    padding=me.Padding(top=16, left=16),
+                    outline="none",
+                    width="100%",
+                    border=me.Border.all(me.BorderSide(style="none")),
+                ),
+            )
+        with me.content_button(type="icon", on_click=send_prompt):
+            me.icon("send")
+
+def send_prompt(e: me.ClickEvent):
+    state = me.state(State)
+    print(f"Sending prompt: {state.input}")
+    state.input = ""
+
+

Here's what we've added:

+
    +
  1. A State class to manage the application state, including the user's input.
  2. +
  3. An on_blur function to update the state when the user switches focus from the textarea.
  4. +
  5. A send_prompt function that will be called when the send button is clicked.
  6. +
+

Running the Updated Application

+

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a styled header, a centered layout, and a functional chat input area.

+

Troubleshooting

+

If you're having trouble, compare your code to the solution.

+

Next Steps

+

In the next section, we'll dive deeper into state management and implement the model picker dialog.

+

+ Managing state & dialogs +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/codelab/3/index.html b/codelab/3/index.html new file mode 100644 index 000000000..2939414f3 --- /dev/null +++ b/codelab/3/index.html @@ -0,0 +1,1695 @@ + + + + + + + + + + + + + + + + + + + + + + + 3 - Managing state & dialogs - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DuoChat Codelab Part 3: Managing state & dialogs

+

In this section, we'll expand our application's state management capabilities and implement a dialog for selecting AI models. We'll use Mesop's state management system and dialog components to create an interactive model selection experience.

+

Expanding the State Management

+

First, let's create a data_model.py file with a more comprehensive state structure:

+
data_model.py
from dataclasses import dataclass, field
+from typing import Literal
+from enum import Enum
+
+import mesop as me
+
+Role = Literal["user", "model"]
+
+@dataclass(kw_only=True)
+class ChatMessage:
+    role: Role = "user"
+    content: str = ""
+    in_progress: bool = False
+
+class Models(Enum):
+    GEMINI_1_5_FLASH = "Gemini 1.5 Flash"
+    GEMINI_1_5_PRO = "Gemini 1.5 Pro"
+    CLAUDE_3_5_SONNET = "Claude 3.5 Sonnet"
+
+@dataclass
+class Conversation:
+    model: str = ""
+    messages: list[ChatMessage] = field(default_factory=list)
+
+@me.stateclass
+class State:
+    is_model_picker_dialog_open: bool = False
+    input: str = ""
+    conversations: list[Conversation] = field(default_factory=list)
+    models: list[str] = field(default_factory=list)
+    gemini_api_key: str = ""
+    claude_api_key: str = ""
+
+@me.stateclass
+class ModelDialogState:
+    selected_models: list[str] = field(default_factory=list)
+
+

This expanded state structure allows us to manage multiple conversations, selected models, and API keys.

+

Implementing the Model Picker Dialog

+

Now, let's implement the model picker dialog in our main.py file. First, we'll create a new file called dialog.py with the following content, which is based on the dialog pattern from the demo gallery:

+
dialog.py
import mesop as me
+
+@me.content_component
+def dialog(is_open: bool):
+    with me.box(
+        style=me.Style(
+            background="rgba(0,0,0,0.4)",
+            display="block" if is_open else "none",
+            height="100%",
+            overflow_x="auto",
+            overflow_y="auto",
+            position="fixed",
+            width="100%",
+            z_index=1000,
+        )
+    ):
+        with me.box(
+            style=me.Style(
+                align_items="center",
+                display="grid",
+                height="100vh",
+                justify_items="center",
+            )
+        ):
+            with me.box(
+                style=me.Style(
+                    background="#fff",
+                    border_radius=20,
+                    box_sizing="content-box",
+                    box_shadow=(
+                        "0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f"
+                    ),
+                    margin=me.Margin.symmetric(vertical="0", horizontal="auto"),
+                    padding=me.Padding.all(20),
+                )
+            ):
+                me.slot()
+
+@me.content_component
+def dialog_actions():
+    with me.box(
+        style=me.Style(
+            display="flex", justify_content="end", margin=me.Margin(top=20)
+        )
+    ):
+        me.slot()
+
+

Now, let's update our main.py file to include the model picker dialog. Copy the following code and replace main.py with it:

+
main.py
# Update the imports:
+import mesop as me
+from data_model import State, Models, ModelDialogState
+from dialog import dialog, dialog_actions
+
+def change_model_option(e: me.CheckboxChangeEvent):
+    s = me.state(ModelDialogState)
+    if e.checked:
+        s.selected_models.append(e.key)
+    else:
+        s.selected_models.remove(e.key)
+
+def set_gemini_api_key(e: me.InputBlurEvent):
+    me.state(State).gemini_api_key = e.value
+
+def set_claude_api_key(e: me.InputBlurEvent):
+    me.state(State).claude_api_key = e.value
+
+def model_picker_dialog():
+    state = me.state(State)
+    with dialog(state.is_model_picker_dialog_open):
+        with me.box(style=me.Style(display="flex", flex_direction="column", gap=12)):
+            me.text("API keys")
+            me.input(
+                label="Gemini API Key",
+                value=state.gemini_api_key,
+                on_blur=set_gemini_api_key,
+            )
+            me.input(
+                label="Claude API Key",
+                value=state.claude_api_key,
+                on_blur=set_claude_api_key,
+            )
+        me.text("Pick a model")
+        for model in Models:
+            if model.name.startswith("GEMINI"):
+                disabled = not state.gemini_api_key
+            elif model.name.startswith("CLAUDE"):
+                disabled = not state.claude_api_key
+            else:
+                disabled = False
+            me.checkbox(
+                key=model.value,
+                label=model.value,
+                checked=model.value in state.models,
+                disabled=disabled,
+                on_change=change_model_option,
+                style=me.Style(
+                    display="flex",
+                    flex_direction="column",
+                    gap=4,
+                    padding=me.Padding(top=12),
+                ),
+            )
+        with dialog_actions():
+            me.button("Cancel", on_click=close_model_picker_dialog)
+            me.button("Confirm", on_click=confirm_model_picker_dialog)
+
+def close_model_picker_dialog(e: me.ClickEvent):
+    state = me.state(State)
+    state.is_model_picker_dialog_open = False
+
+def confirm_model_picker_dialog(e: me.ClickEvent):
+    dialog_state = me.state(ModelDialogState)
+    state = me.state(State)
+    state.is_model_picker_dialog_open = False
+    state.models = dialog_state.selected_models
+
+ROOT_BOX_STYLE = me.Style(
+    background="#e7f2ff",
+    height="100%",
+    font_family="Inter",
+    display="flex",
+    flex_direction="column",
+)
+
+@me.page(
+    path="/",
+    stylesheets=[
+        "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
+    ],
+)
+def page():
+    model_picker_dialog()
+    with me.box(style=ROOT_BOX_STYLE):
+        header()
+        with me.box(
+            style=me.Style(
+                width="min(680px, 100%)",
+                margin=me.Margin.symmetric(horizontal="auto", vertical=36),
+            )
+        ):
+            me.text(
+                "Chat with multiple models at once",
+                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),
+            )
+            chat_input()
+
+def header():
+    with me.box(
+        style=me.Style(
+            padding=me.Padding.all(16),
+        ),
+    ):
+        me.text(
+            "DuoChat",
+            style=me.Style(
+                font_weight=500,
+                font_size=24,
+                color="#3D3929",
+                letter_spacing="0.3px",
+            ),
+        )
+
+def switch_model(e: me.ClickEvent):
+    state = me.state(State)
+    state.is_model_picker_dialog_open = True
+    dialog_state = me.state(ModelDialogState)
+    dialog_state.selected_models = state.models[:]
+
+def chat_input():
+    state = me.state(State)
+    with me.box(
+        style=me.Style(
+            border_radius=16,
+            padding=me.Padding.all(8),
+            background="white",
+            display="flex",
+            width="100%",
+        )
+    ):
+        with me.box(style=me.Style(flex_grow=1)):
+            me.native_textarea(
+                value=state.input,
+                placeholder="Enter a prompt",
+                on_blur=on_blur,
+                style=me.Style(
+                    padding=me.Padding(top=16, left=16),
+                    outline="none",
+                    width="100%",
+                    border=me.Border.all(me.BorderSide(style="none")),
+                ),
+            )
+            with me.box(
+                style=me.Style(
+                    display="flex",
+                    padding=me.Padding(left=12, bottom=12),
+                    cursor="pointer",
+                ),
+                on_click=switch_model,
+            ):
+                me.text(
+                    "Model:",
+                    style=me.Style(font_weight=500, padding=me.Padding(right=6)),
+                )
+                if state.models:
+                    me.text(", ".join(state.models))
+                else:
+                    me.text("(no model selected)")
+        with me.content_button(
+            type="icon", on_click=send_prompt, disabled=not state.models
+        ):
+            me.icon("send")
+
+def on_blur(e: me.InputBlurEvent):
+    state = me.state(State)
+    state.input = e.value
+
+def send_prompt(e: me.ClickEvent):
+    state = me.state(State)
+    print(f"Sending prompt: {state.input}")
+    print(f"Selected models: {state.models}")
+    state.input = ""
+
+

This updated code adds the following features:

+
    +
  1. A model picker dialog that allows users to select AI models and enter API keys.
  2. +
  3. State management for selected models and API keys.
  4. +
  5. A model switcher in the chat input area that opens the model picker dialog.
  6. +
  7. Disabling of models based on whether the corresponding API key has been entered.
  8. +
+

Running the Updated Application

+

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a chat input area with a model switcher. Clicking on the model switcher will open the model picker dialog, allowing you to select models and enter API keys.

+

Troubleshooting

+

If you're having trouble, compare your code to the solution.

+

Next Steps

+

In the next section, we'll integrate multiple AI models into our application. We'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a way to interact with multiple models.

+

+ Integrating AI APIs +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/codelab/4/index.html b/codelab/4/index.html new file mode 100644 index 000000000..9a3575cc2 --- /dev/null +++ b/codelab/4/index.html @@ -0,0 +1,1621 @@ + + + + + + + + + + + + + + + + + + + + + + + 4 - Integrating AI APIs - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DuoChat Codelab Part 4: Integrating AI APIs

+

In this section, we'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a unified interface for interacting with multiple models simultaneously.

+

Setting Up AI Model Connections

+

First, let's create separate files for our Gemini and Claude integrations.

+

Create a new file called gemini.py:

+
gemini.py
import google.generativeai as genai
+from typing import Iterable
+
+from data_model import ChatMessage, State
+import mesop as me
+
+generation_config = {
+    "temperature": 1,
+    "top_p": 0.95,
+    "top_k": 64,
+    "max_output_tokens": 8192,
+}
+
+def configure_gemini():
+    state = me.state(State)
+    genai.configure(api_key=state.gemini_api_key)
+
+def send_prompt_pro(prompt: str, history: list[ChatMessage]) -> Iterable[str]:
+    configure_gemini()
+    model = genai.GenerativeModel(
+        model_name="gemini-1.5-pro-latest",
+        generation_config=generation_config,
+    )
+    chat_session = model.start_chat(
+        history=[
+            {"role": message.role, "parts": [message.content]} for message in history
+        ]
+    )
+    for chunk in chat_session.send_message(prompt, stream=True):
+        yield chunk.text
+
+def send_prompt_flash(prompt: str, history: list[ChatMessage]) -> Iterable[str]:
+    configure_gemini()
+    model = genai.GenerativeModel(
+        model_name="gemini-1.5-flash-latest",
+        generation_config=generation_config,
+    )
+    chat_session = model.start_chat(
+        history=[
+            {"role": message.role, "parts": [message.content]} for message in history
+        ]
+    )
+    for chunk in chat_session.send_message(prompt, stream=True):
+        yield chunk.text
+
+

Now, create a new file called claude.py:

+
claude.py
import anthropic
+from typing import Iterable
+
+from data_model import ChatMessage, State
+import mesop as me
+
+def call_claude_sonnet(input: str, history: list[ChatMessage]) -> Iterable[str]:
+    state = me.state(State)
+    client = anthropic.Anthropic(api_key=state.claude_api_key)
+    messages = [
+        {
+            "role": "assistant" if message.role == "model" else message.role,
+            "content": message.content,
+        }
+        for message in history
+    ] + [{"role": "user", "content": input}]
+
+    with client.messages.stream(
+        max_tokens=1024,
+        messages=messages,
+        model="claude-3-sonnet-20240229",
+    ) as stream:
+        for text in stream.text_stream:
+            yield text
+
+

Updating the Main Application

+

Now, let's update our main.py file to integrate these AI models. We'll update the page function, add the display_conversations and display_message functions and modify the send_prompt function to handle multiple models and display their responses:

+
main.py
import mesop as me
+from data_model import State, Models, ModelDialogState, Conversation, ChatMessage
+from dialog import dialog, dialog_actions
+import claude
+import gemini
+
+# ... (keep the existing imports and styles)
+
+# Replace page() with this:
+@me.page(
+    path="/",
+    stylesheets=[
+        "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
+    ],
+)
+def page():
+    model_picker_dialog()
+    with me.box(style=ROOT_BOX_STYLE):
+        header()
+        with me.box(
+            style=me.Style(
+                width="min(680px, 100%)",
+                margin=me.Margin.symmetric(horizontal="auto", vertical=36),
+            )
+        ):
+            me.text(
+                "Chat with multiple models at once",
+                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),
+            )
+            chat_input()
+            display_conversations()
+
+# Add display_conversations and display_message:
+def display_conversations():
+    state = me.state(State)
+    for conversation in state.conversations:
+        with me.box(style=me.Style(margin=me.Margin(bottom=24))):
+            me.text(f"Model: {conversation.model}", style=me.Style(font_weight=500))
+            for message in conversation.messages:
+                display_message(message)
+
+def display_message(message: ChatMessage):
+    style = me.Style(
+        padding=me.Padding.all(12),
+        border_radius=8,
+        margin=me.Margin(bottom=8),
+    )
+    if message.role == "user":
+        style.background = "#e7f2ff"
+    else:
+        style.background = "#ffffff"
+
+    with me.box(style=style):
+        me.markdown(message.content)
+        if message.in_progress:
+            me.progress_spinner()
+
+# Update send_prompt:
+def send_prompt(e: me.ClickEvent):
+    state = me.state(State)
+    if not state.conversations:
+        for model in state.models:
+            state.conversations.append(Conversation(model=model, messages=[]))
+    input = state.input
+    state.input = ""
+
+    for conversation in state.conversations:
+        model = conversation.model
+        messages = conversation.messages
+        history = messages[:]
+        messages.append(ChatMessage(role="user", content=input))
+        messages.append(ChatMessage(role="model", in_progress=True))
+        yield
+
+        if model == Models.GEMINI_1_5_FLASH.value:
+            llm_response = gemini.send_prompt_flash(input, history)
+        elif model == Models.GEMINI_1_5_PRO.value:
+            llm_response = gemini.send_prompt_pro(input, history)
+        elif model == Models.CLAUDE_3_5_SONNET.value:
+            llm_response = claude.call_claude_sonnet(input, history)
+        else:
+            raise Exception("Unhandled model", model)
+
+        for chunk in llm_response:
+            messages[-1].content += chunk
+            yield
+        messages[-1].in_progress = False
+        yield
+
+

This updated code adds the following features:

+
    +
  1. A display_conversations function that shows the chat history for each selected model.
  2. +
  3. A display_message function that renders individual messages with appropriate styling.
  4. +
  5. An updated send_prompt function that sends the user's input to all selected models and displays their responses in real-time.
  6. +
+

Handling Streaming Responses

+

The send_prompt function now uses Python generators to handle streaming responses from the AI models. This allows us to update the UI in real-time as the models generate their responses.

+

Running the Updated Application

+

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now be able to:

+
    +
  1. Select multiple AI models using the model picker dialog.
  2. +
  3. Enter API keys for Gemini and Claude.
  4. +
  5. Send prompts to the selected models.
  6. +
  7. See the responses from multiple models displayed simultaneously.
  8. +
+

Troubleshooting

+

If you're having trouble, compare your code to the solution.

+

Next Steps

+

In the final section, we'll refine the user interface by creating a dedicated conversation page with a multi-column layout for different model responses. We'll also add some finishing touches to improve the overall user experience.

+

+ Wrapping it up +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/codelab/5/index.html b/codelab/5/index.html new file mode 100644 index 000000000..ba0750fe0 --- /dev/null +++ b/codelab/5/index.html @@ -0,0 +1,1726 @@ + + + + + + + + + + + + + + + + + + + + + + + 5 - Wrapping it up - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

DuoChat Codelab Part 5: Wrapping it up

+

In this section, we'll create a multi-column layout for different model responses, implement user and model message components, add auto-scroll functionality, and finalize the chat experience with multiple models.

+

Creating a New Conversation Page

+

First, let's create a new page for our conversations. Update the main.py file to include a new route and function for the conversation page:

+
main.py
import mesop as me
+from data_model import State, Models, ModelDialogState, Conversation, ChatMessage
+from dialog import dialog, dialog_actions
+import claude
+import gemini
+
+# ... (keep the existing imports and styles)
+
+STYLESHEETS = [
+  "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap"
+]
+
+@me.page(
+    path="/",
+    stylesheets=STYLESHEETS,
+)
+def home_page():
+    model_picker_dialog()
+    with me.box(style=ROOT_BOX_STYLE):
+        header()
+        with me.box(
+            style=me.Style(
+                width="min(680px, 100%)",
+                margin=me.Margin.symmetric(horizontal="auto", vertical=36),
+            )
+        ):
+            me.text(
+                "Chat with multiple models at once",
+                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),
+            )
+            # Uncomment this in the next step:
+            # examples_row()
+            chat_input()
+
+@me.page(path="/conversation", stylesheets=STYLESHEETS)
+def conversation_page():
+    state = me.state(State)
+    model_picker_dialog()
+    with me.box(style=ROOT_BOX_STYLE):
+        header()
+
+        models = len(state.conversations)
+        models_px = models * 680
+        with me.box(
+            style=me.Style(
+                width=f"min({models_px}px, calc(100% - 32px))",
+                display="grid",
+                gap=16,
+                grid_template_columns=f"repeat({models}, 1fr)",
+                flex_grow=1,
+                overflow_y="hidden",
+                margin=me.Margin.symmetric(horizontal="auto"),
+                padding=me.Padding.symmetric(horizontal=16),
+            )
+        ):
+            for conversation in state.conversations:
+                model = conversation.model
+                messages = conversation.messages
+                with me.box(
+                    style=me.Style(
+                        overflow_y="auto",
+                    )
+                ):
+                    me.text("Model: " + model, style=me.Style(font_weight=500))
+
+                    for message in messages:
+                        if message.role == "user":
+                            user_message(message.content)
+                        else:
+                            model_message(message)
+                    if messages and model == state.conversations[-1].model:
+                        me.box(
+                            key="end_of_messages",
+                            style=me.Style(
+                                margin=me.Margin(
+                                    bottom="50vh" if messages[-1].in_progress else 0
+                                )
+                            ),
+                        )
+        with me.box(
+            style=me.Style(
+                display="flex",
+                justify_content="center",
+            )
+        ):
+            with me.box(
+                style=me.Style(
+                    width="min(680px, 100%)",
+                    padding=me.Padding(top=24, bottom=24),
+                )
+            ):
+                chat_input()
+
+def user_message(content: str):
+    with me.box(
+        style=me.Style(
+            background="#e7f2ff",
+            padding=me.Padding.all(16),
+            margin=me.Margin.symmetric(vertical=16),
+            border_radius=16,
+        )
+    ):
+        me.text(content)
+
+def model_message(message: ChatMessage):
+    with me.box(
+        style=me.Style(
+            background="#fff",
+            padding=me.Padding.all(16),
+            border_radius=16,
+            margin=me.Margin.symmetric(vertical=16),
+        )
+    ):
+        me.markdown(message.content)
+        if message.in_progress:
+            me.progress_spinner()
+
+# ... (keep the existing helper functions)
+
+def send_prompt(e: me.ClickEvent):
+    state = me.state(State)
+    if not state.conversations:
+        me.navigate("/conversation")
+        for model in state.models:
+            state.conversations.append(Conversation(model=model, messages=[]))
+    input = state.input
+    state.input = ""
+
+    for conversation in state.conversations:
+        model = conversation.model
+        messages = conversation.messages
+        history = messages[:]
+        messages.append(ChatMessage(role="user", content=input))
+        messages.append(ChatMessage(role="model", in_progress=True))
+        yield
+        me.scroll_into_view(key="end_of_messages")
+        if model == Models.GEMINI_1_5_FLASH.value:
+            llm_response = gemini.send_prompt_flash(input, history)
+        elif model == Models.GEMINI_1_5_PRO.value:
+            llm_response = gemini.send_prompt_pro(input, history)
+        elif model == Models.CLAUDE_3_5_SONNET.value:
+            llm_response = claude.call_claude_sonnet(input, history)
+        else:
+            raise Exception("Unhandled model", model)
+        for chunk in llm_response:
+            messages[-1].content += chunk
+            yield
+        messages[-1].in_progress = False
+        yield
+
+

Try running the app: mesop main.py and now you should navigate to the conversation page once you click the send button.

+

Adding Example Prompts

+

Let's add some example prompts to the home page to help users get started. Add the following functions to main.py:

+
main.py
EXAMPLES = [
+    "Create a file-lock in Python",
+    "Write an email to Congress to have free milk for all",
+    "Make a nice box shadow in CSS",
+]
+
+def examples_row():
+    with me.box(
+        style=me.Style(
+            display="flex", flex_direction="row", gap=16, margin=me.Margin(bottom=24)
+        )
+    ):
+        for i in EXAMPLES:
+            example(i)
+
+def example(text: str):
+    with me.box(
+        key=text,
+        on_click=click_example,
+        style=me.Style(
+            cursor="pointer",
+            background="#b9e1ff",
+            width="215px",
+            height=160,
+            font_weight=500,
+            line_height="1.5",
+            padding=me.Padding.all(16),
+            border_radius=16,
+            border=me.Border.all(me.BorderSide(width=1, color="blue", style="none")),
+        ),
+    ):
+        me.text(text)
+
+def click_example(e: me.ClickEvent):
+    state = me.state(State)
+    state.input = e.key
+
+

And then uncomment the callsite for examples_row in home_page.

+

Updating the Header

+

Let's update header to allow users to return to the home page when they click on the header box:

+
main.py
def header():
+    def navigate_home(e: me.ClickEvent):
+        me.navigate("/")
+        state = me.state(State)
+        state.conversations = []
+
+    with me.box(
+        on_click=navigate_home,
+        style=me.Style(
+            cursor="pointer",
+            padding=me.Padding.all(16),
+        ),
+    ):
+        me.text(
+            "DuoChat",
+            style=me.Style(
+                font_weight=500,
+                font_size=24,
+                color="#3D3929",
+                letter_spacing="0.3px",
+            ),
+        )
+
+

Finalizing the Chat Experience

+

Now that we have a dedicated conversation page with a multi-column layout, let's make some final improvements to enhance the user experience:

+
    +
  1. Navigate to the conversations page at the start of a conversation.
  2. +
  3. Implement auto-scrolling to keep the latest messages in view.
  4. +
+

Update the send_prompt function in main.py:

+
main.py
def send_prompt(e: me.ClickEvent):
+    state = me.state(State)
+    if not state.conversations:
+        me.navigate("/conversation")
+        for model in state.models:
+            state.conversations.append(Conversation(model=model, messages=[]))
+    input = state.input
+    state.input = ""
+
+    for conversation in state.conversations:
+        model = conversation.model
+        messages = conversation.messages
+        history = messages[:]
+        messages.append(ChatMessage(role="user", content=input))
+        messages.append(ChatMessage(role="model", in_progress=True))
+        yield
+        me.scroll_into_view(key="end_of_messages")
+        if model == Models.GEMINI_1_5_FLASH.value:
+            llm_response = gemini.send_prompt_flash(input, history)
+        elif model == Models.GEMINI_1_5_PRO.value:
+            llm_response = gemini.send_prompt_pro(input, history)
+        elif model == Models.CLAUDE_3_5_SONNET.value:
+            llm_response = claude.call_claude_sonnet(input, history)
+        else:
+            raise Exception("Unhandled model", model)
+        for chunk in llm_response:
+            messages[-1].content += chunk
+            yield
+        messages[-1].in_progress = False
+        yield
+
+

Running the Final Application

+

Run the application with mesop main.py and navigate to http://localhost:32123. You should now have a fully functional DuoChat application with the following features:

+
    +
  1. A home page with example prompts and a model picker.
  2. +
  3. A conversation page with a multi-column layout for different model responses.
  4. +
  5. Real-time streaming of model responses with loading indicators.
  6. +
  7. Auto-scrolling to keep the latest message in view.
  8. +
+

Troubleshooting

+

If you're having trouble, compare your code to the solution.

+

Conclusion

+

Congratulations! You've successfully built DuoChat, a Mesop application that allows users to interact with multiple AI models simultaneously. This project demonstrates Mesop's capabilities for creating responsive UIs, managing complex state, and integrating with external APIs.

+

Some potential next steps to further improve the application:

+
    +
  1. Deploy your app.
  2. +
  3. Add the ability to save and load conversations.
  4. +
  5. Add support for additional AI models or services.
  6. +
+

Feel free to experiment with these ideas or come up with your own improvements to enhance the DuoChat experience!

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/codelab/index.html b/codelab/index.html new file mode 100644 index 000000000..fabf1e21e --- /dev/null +++ b/codelab/index.html @@ -0,0 +1,1515 @@ + + + + + + + + + + + + + + + + + + + + + + + 1 - Overview & setup - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Mesop DuoChat Codelab

+

This tutorial walks you through building DuoChat, an interactive web application for chatting with multiple AI models simultaneously. You'll learn how to leverage Mesop's powerful features to create a responsive UI and integrate with AI APIs like Google Gemini and Anthropic Claude.

+

What you will build

+

By the end of this codelab, you will build DuoChat (demo) that will allow users to:

+
    +
  • Select multiple AI models to chat with
  • +
  • Compare responses from different models side-by-side
  • +
  • Provide their own API keys
  • +
+

If you want to dive straight into the code, you can look at the DuoChat repo and each branch represents the completed code after each section.

+

Setting Up the Development Environment

+

Let's start by setting up our development environment:

+
    +
  1. Create a new directory for your project:
  2. +
+
mkdir duochat
+cd duochat
+
+
    +
  1. +

    Follow the Mesop command-line installation guide and create a virtual environment and activate it.

    +
  2. +
  3. +

    Create a requirements.txt file with the following content:

    +
  4. +
+
mesop
+gunicorn
+anthropic
+google-generativeai
+
+
    +
  1. Install the dependencies:
  2. +
+
pip install -r requirements.txt
+
+

Setting Up the Main Application

+

Let's start by creating a basic Mesop application. Create main.py and add the following code:

+
main.py
import mesop as me
+
+@me.page(path="/")
+def page():
+    me.text("Welcome to DuoChat!")
+
+

This creates a simple Mesop application with a welcome message.

+

Running the Application

+

To run your Mesop application:

+
mesop main.py
+
+

Navigate to http://localhost:32123 in your web browser. You should see the welcome message.

+

Getting API keys

+

Later on, you will need API keys to call the respective AI models:

+ +
+

TIP: You can get started with the Gemini API key, which has a free tier, first and create the Anthropic API key later.

+
+

Troubleshooting

+

If you're having trouble, compare your code to the solution.

+

Next Steps

+

In the next section, we'll start building the user interface for DuoChat, including the header, chat input area, and basic styling. We'll explore Mesop's components and styling system to create an attractive and functional layout.

+

+ Building the basic UI +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/comparison/index.html b/comparison/index.html new file mode 100644 index 000000000..5ceea9dfe --- /dev/null +++ b/comparison/index.html @@ -0,0 +1,1331 @@ + + + + + + + + + + + + + + + + + + + + + + + Comparison - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Comparison with Other Python UI Frameworks

+

This page aims to provide an objective comparison between Mesop and other popular Python-based web application frameworks, specifically Streamlit and Gradio. This is a difficult doc to write but we feel that it's important to explain the differences as this is frequently asked.

+

While we believe Mesop offers a unique philosophy for building UIs, we strive to be fair and accurate in highlighting the strengths of each framework.

+

Because this is a fast-moving space, some of the information may be out of date. Please file an issue and let us know what we should fix.

+

Streamlit

+

Streamlit and Mesop share similar goals in terms of enabling Python developers to rapidly build web apps, particularly for AI use cases.

+

However, there are some key differences:

+

Execution Model

+

Streamlit executes apps in a script-like manner where the entire app reruns on each user interaction. This enables a boilerplate-free UI development model that's easy to get started with, but requires mechanisms like caching and fragments to optimize the performance with this model.

+

Mesop uses a function-based model commonly found in web frameworks where the program is executed once on server initialization and then the page and component functions are executed in each render loop. This provides regular Python execution semantics because top-level initialization code is executed exactly once.

+

Styling and Customization

+

Streamlit offers pre-styled components with customization primarily through themes, prioritizing consistency and ease of use over flexibility.

+

In addition to providing Material-themed components, Mesop offers a low-level Style API to configure CSS properties. Mesop provides limited theming support with dark theming and doesn't support theming to other colors.

+

Components

+

Both Streamlit and Mesop offer a range of standard components (e.g., forms, tables, chat interfaces), with Streamlit providing a larger set of built-in components, especially for data science use cases like data visualization.

+

Streamlit supports custom components rendered in iframes for isolation. It offers first-class support for React components and can accommodate other frameworks through a framework-agnostic template.

+

Mesop enables creating custom web components based on open web standards, facilitating interoperability with components from different frameworks like Lit. Mesop web components are rendered in the same frame as the rest of the Mesop app which provides more flexibility but less isolation.

+

Streamlit has a more established ecosystem of community-developed components, while Mesop's community and component ecosystem are still developing.

+

Gradio

+

Gradio and Mesop both enable rapid ML/AI app development but with different approaches.

+

Gradio has a strong focus on creating demos and interfaces for machine learning models and makes it easy to build a UI for a model. Gradio also offers a lower-level abstraction known as Blocks for more general web applications.

+

Mesop, while well-suited for ML/AI use cases, is a more general-purpose framework that can be used for a wide range of web applications.

+

Components

+

Gradio provides a set of pre-built components optimized for common ML inputs and outputs (e.g. image classification, text generation). This makes it fast to set up standard model interfaces. In addition to built-in components, you can create custom components in Python and JavaScript (Svelte).

+

Mesop provides general-purpose UI components, which can be used for a variety of layout and UI designs. Higher-level components like the chat component are built on top of these low-level UI components. This makes it better suited for building custom interfaces, such as the demo gallery. Mesop also supports creating custom web components based on open web standards, facilitating interoperability with components from different frameworks.

+

Styling and Customization

+

Gradio features a robust theming system with prebuilt options and extensive UI customization. It also supports custom CSS via direct string construction.

+

Mesop offers a statically typed Style API for CSS properties. While it includes dark theme support, Mesop's theming capabilities are currently limited and does not allow custom color schemes.

+

State management

+

Gradio uses an imperative approach to state management, coupling state with component updates. State is typically managed through function parameters and return values, which can be straightforward for simple interfaces but may become complex as applications grow.

+

Mesop adopts a declarative state management approach, separating state updates from UI rendering. It uses dataclasses for state, providing type-safety and structure for complex states. This separation allows for more granular control over UI updates but may have a steeper learning curve for beginners.

+

Deployment

+

Gradio makes it easy to share demos via Hugging Face Spaces. Mesop apps can also be deployed on Hugging Face Spaces, but requires a few more steps.

+

Conclusion

+

Both Streamlit and Gradio offer gentle learning curves, making it easy for Python developers to quickly build standard AI applications.

+

Mesop embraces a declarative UI paradigm, which introduces additional concepts but can provide more flexibility for custom applications.

+

Ultimately, the best choice depends on your specific use case, desired level of customization, and development preferences. We encourage you to explore each framework to determine which best fits your needs.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/audio/index.html b/components/audio/index.html new file mode 100644 index 000000000..e73f60795 --- /dev/null +++ b/components/audio/index.html @@ -0,0 +1,2523 @@ + + + + + + + + + + + + + + + + + + + + + + + Audio - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Audio

+ +

Overview

+

Audio is the equivalent of an <audio> HTML element. Audio displays the browser's native audio controls.

+

Examples

+ + +
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="/audio",
+)
+def app():
+  """
+  In order to autoplay audio, set the `autoplay` attribute to `True`,
+  Note that there are autoplay restrictions in modern browsers, including Chrome,
+  are designed to prevent audio or video from playing automatically without user interaction.
+  This is intended to improve user experience and reduce unwanted interruptions.
+  You can check the [autoplay ability of your application](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability)
+  """
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.audio(
+      src="https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3",
+      # autoplay=True
+    )
+
+

API

+ + +
+ + +

+ audio + +

+ + +
+ +

Creates an audio component.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
src +
+

The URL of the audio to be played.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
autoplay +
+

boolean value indicating if the audio should be autoplayed or not. Note: There are autoplay restrictions in modern browsers, including Chrome, are designed to prevent audio or video from playing automatically without user interaction. This is intended to improve user experience and reduce unwanted interruptions

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/autocomplete/index.html b/components/autocomplete/index.html new file mode 100644 index 000000000..25899fcf2 --- /dev/null +++ b/components/autocomplete/index.html @@ -0,0 +1,3371 @@ + + + + + + + + + + + + + + + + + + + + + + + Autocomplete - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Autocomplete

+ +

Overview

+

Autocomplete allows the user to enter free text or select from a list of dynamic values +and is based on the Angular Material autocomplete component.

+

This components only renders text labels and values.

+

The autocomplete filters by case-insensitively matching substrings of the option label.

+

Currently, there is no on blur event with this component since the blur event does not +get the selected value on the first blur. Due to this ambiguous behavior, the blur event +has been left out.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  raw_value: str
+  selected_value: str = "California"
+
+
+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="/autocomplete",
+)
+def app():
+  state = me.state(State)
+
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.autocomplete(
+      label="Select state",
+      value=state.selected_value,
+      options=_make_autocomplete_options(),
+      on_selection_change=on_value_change,
+      on_enter=on_value_change,
+      on_input=on_input,
+      appearance="outline",
+    )
+
+    if state.selected_value:
+      me.text("Selected: " + state.selected_value)
+
+
+def on_value_change(
+  e: me.AutocompleteEnterEvent | me.AutocompleteSelectionChangeEvent,
+):
+  state = me.state(State)
+  state.selected_value = e.value
+
+
+def on_input(e: me.InputEvent):
+  state = me.state(State)
+  state.raw_value = e.value
+
+
+def _make_autocomplete_options() -> list[me.AutocompleteOptionGroup]:
+  """Creates and filter autocomplete options.
+
+  The states list assumed to be alphabetized and we group by the first letter of the
+  state's name.
+  """
+  states_options_list = []
+  sub_group = None
+  for state in _STATES:
+    if not sub_group or sub_group.label != state[0]:
+      if sub_group:
+        states_options_list.append(sub_group)
+      sub_group = me.AutocompleteOptionGroup(label=state[0], options=[])
+    sub_group.options.append(me.AutocompleteOption(label=state, value=state))
+  if sub_group:
+    states_options_list.append(sub_group)
+  return states_options_list
+
+
+_STATES = [
+  "Alabama",
+  "Alaska",
+  "Arizona",
+  "Arkansas",
+  "California",
+  "Colorado",
+  "Connecticut",
+  "Delaware",
+  "Florida",
+  "Georgia",
+  "Hawaii",
+  "Idaho",
+  "Illinois",
+  "Indiana",
+  "Iowa",
+  "Kansas",
+  "Kentucky",
+  "Louisiana",
+  "Maine",
+  "Maryland",
+  "Massachusetts",
+  "Michigan",
+  "Minnesota",
+  "Mississippi",
+  "Missouri",
+  "Montana",
+  "Nebraska",
+  "Nevada",
+  "New Hampshire",
+  "New Jersey",
+  "New Mexico",
+  "New York",
+  "North Carolina",
+  "North Dakota",
+  "Ohio",
+  "Oklahoma",
+  "Oregon",
+  "Pennsylvania",
+  "Rhode Island",
+  "South Carolina",
+  "South Dakota",
+  "Tennessee",
+  "Texas",
+  "Utah",
+  "Vermont",
+  "Virginia",
+  "Washington",
+  "West Virginia",
+  "Wisconsin",
+  "Wyoming",
+]
+
+

API

+ + +
+ + +

+ autocomplete + +

+ + +
+ +

Creates an autocomplete component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
options +
+

Selectable options from autocomplete.

+
+

+ + TYPE: + Iterable[AutocompleteOption | AutocompleteOptionGroup] | None + + + DEFAULT: + None + +

+
label +
+

Label for input.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
on_selection_change +
+

Event emitted when the selected value has been changed by the user.

+
+

+ + TYPE: + Callable[[AutocompleteSelectionChangeEvent], Any] | None + + + DEFAULT: + None + +

+
on_input +
+

input is fired whenever the input has changed (e.g. user types).

+
+

+ + TYPE: + Callable[[InputEvent], Any] | None + + + DEFAULT: + None + +

+
on_enter +
+

triggers when the browser detects an "Enter" key on a keyup native browser event.

+
+

+ + TYPE: + Callable[[AutocompleteEnterEvent], Any] | None + + + DEFAULT: + None + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder +
+

Placeholder value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
value +
+

Initial value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_required_marker +
+

Whether the required marker should be hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

The color palette for the form field.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
float_label +
+

Whether the label should always float or float as the user types.

+
+

+ + TYPE: + Literal['always', 'auto'] + + + DEFAULT: + 'auto' + +

+
subscript_sizing +
+

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

+
+

+ + TYPE: + Literal['fixed', 'dynamic'] + + + DEFAULT: + 'fixed' + +

+
hint_label +
+

Text for the form field hint.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
style +
+

Style for input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ AutocompleteOption + + + dataclass + + +

+ + +
+ + +

Represents an option in the autocomplete drop down.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
label +
+

Content to show for the autocomplete option

+
+

+ + TYPE: + str | None + +

+
value +
+

The value of this autocomplete option.

+
+

+ + TYPE: + str | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ AutocompleteOptionGroup + + + dataclass + + +

+ + +
+ + +

Represents an option group to group options in the autocomplete drop down.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
label +
+

Group label

+
+

+ + TYPE: + str + +

+
options +
+

Autocomplete options under this group

+
+

+ + TYPE: + list[AutocompleteOption] + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ AutocompleteEnterEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents an "Enter" keyboard event on an autocomplete component.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input/selected value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ AutocompleteSelectionChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a selection change event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Selected value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ InputEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a user input event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/badge/index.html b/components/badge/index.html new file mode 100644 index 000000000..9ad83aec6 --- /dev/null +++ b/components/badge/index.html @@ -0,0 +1,2630 @@ + + + + + + + + + + + + + + + + + + + + + + + Badge - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Badge

+ +

Overview

+

Badge decorates a UI component and is oftentimes used for unread message count and is based on the Angular Material badge component.

+

Examples

+ + +
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="/badge",
+)
+def app():
+  with me.box(
+    style=me.Style(
+      display="block",
+      padding=me.Padding(top=16, right=16, bottom=16, left=16),
+      height=50,
+      width=30,
+    )
+  ):
+    with me.badge(content="1", size="medium"):
+      me.text(text="text with badge")
+
+

API

+ + +
+ + +

+ badge + +

+ + +
+ +

Creates a Badge component. +Badge is a composite component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
color +
+

The color of the badge. Can be primary, accent, or warn.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
overlap +
+

Whether the badge should overlap its contents or not

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the badge is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
position +
+

Position the badge should reside. Accepts any combination of 'above'|'below' and 'before'|'after'

+
+

+ + TYPE: + Literal['above after', 'above before', 'below before', 'below after', 'before', 'after', 'above', 'below'] + + + DEFAULT: + 'above after' + +

+
content +
+

The content for the badge

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
description +
+

Message used to describe the decorated element via aria-describedby

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
size +
+

Size of the badge. Can be 'small', 'medium', or 'large'.

+
+

+ + TYPE: + Literal['small', 'medium', 'large'] + + + DEFAULT: + 'small' + +

+
hidden +
+

Whether the badge is hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/box/index.html b/components/box/index.html new file mode 100644 index 000000000..d88aff623 --- /dev/null +++ b/components/box/index.html @@ -0,0 +1,2605 @@ + + + + + + + + + + + + + + + + + + + + + + + Box - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Box

+ +

Overview

+

Box is a content component which acts as a container to group children components and styling them.

+

Examples

+ + +
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="/box",
+)
+def app():
+  with me.box(style=me.Style(background="red", padding=me.Padding.all(16))):
+    with me.box(
+      style=me.Style(
+        background="green",
+        height=50,
+        margin=me.Margin.symmetric(vertical=24, horizontal=12),
+        border=me.Border.symmetric(
+          horizontal=me.BorderSide(width=2, color="pink", style="solid"),
+          vertical=me.BorderSide(width=2, color="orange", style="solid"),
+        ),
+      )
+    ):
+      me.text(text="hi1")
+      me.text(text="hi2")
+
+    with me.box(
+      style=me.Style(
+        background="blue",
+        height=50,
+        margin=me.Margin.all(16),
+        border=me.Border.all(
+          me.BorderSide(width=2, color="yellow", style="dotted")
+        ),
+        border_radius=10,
+      )
+    ):
+      me.text(text="Example with all sides bordered")
+
+    with me.box(
+      style=me.Style(
+        background="purple",
+        height=50,
+        margin=me.Margin.symmetric(vertical=24, horizontal=12),
+        border=me.Border.symmetric(
+          vertical=me.BorderSide(width=4, color="white", style="double")
+        ),
+      )
+    ):
+      me.text(text="Example with top and bottom borders")
+
+    with me.box(
+      style=me.Style(
+        background="cyan",
+        height=50,
+        margin=me.Margin.symmetric(vertical=24, horizontal=12),
+        border=me.Border.symmetric(
+          horizontal=me.BorderSide(width=2, color="black", style="groove")
+        ),
+      )
+    ):
+      me.text(text="Example with left and right borders")
+
+

API

+ + +
+ + +

+ box + +

+ + +
+ +

Creates a box component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
style +
+

Style to apply to component. Follows HTML Element inline style API.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
on_click +
+

The callback function that is called when the box is clicked. +It receives a ClickEvent as its only argument.

+
+

+ + TYPE: + Callable[[ClickEvent], Any] | None + + + DEFAULT: + None + +

+
classes +
+

CSS classes.

+
+

+ + TYPE: + list[str] | str + + + DEFAULT: + '' + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ + + + + + + + + + + + + + + +
RETURNSDESCRIPTION
+ + Any + + +
+

The created box component.

+
+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/button/index.html b/components/button/index.html new file mode 100644 index 000000000..dfd206a65 --- /dev/null +++ b/components/button/index.html @@ -0,0 +1,2891 @@ + + + + + + + + + + + + + + + + + + + + + + + Button - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Button

+ +

Overview

+

Button is based on the Angular Material button component.

+

Examples

+ + +
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="/button",
+)
+def main():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text("Button types:", style=me.Style(margin=me.Margin(bottom=12)))
+    with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+      me.button("default")
+      me.button("raised", type="raised")
+      me.button("flat", type="flat")
+      me.button("stroked", type="stroked")
+
+    me.text(
+      "Button colors:", style=me.Style(margin=me.Margin(top=12, bottom=12))
+    )
+    with me.box(style=me.Style(display="flex", flex_direction="row", gap=12)):
+      me.button("default", type="flat")
+      me.button("primary", color="primary", type="flat")
+      me.button("secondary", color="accent", type="flat")
+      me.button("warn", color="warn", type="flat")
+
+

API

+ + +
+ + +

+ button + +

+ + +
+ +

Creates a simple text Button component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Text label for button

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
on_click +
+

click is a native browser event.

+
+

+ + TYPE: + Callable[[ClickEvent], Any] | None + + + DEFAULT: + None + +

+
type +
+

Type of button style to use

+
+

+ + TYPE: + Literal['raised', 'flat', 'stroked'] | None + + + DEFAULT: + None + +

+
color +
+

Theme color palette of the button

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
disable_ripple +
+

Whether the ripple effect is disabled or not.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the button is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + +

+ content_button + +

+ + +
+ +

Creates a button component, which is a composite component. Typically, you would use a text or icon component as a child.

+

Intended for advanced use cases.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
on_click +
+

click is a native browser event.

+
+

+ + TYPE: + Callable[[ClickEvent], Any] | None + + + DEFAULT: + None + +

+
type +
+

Type of button style to use

+
+

+ + TYPE: + Literal['raised', 'flat', 'stroked', 'icon'] | None + + + DEFAULT: + None + +

+
color +
+

Theme color palette of the button

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
disable_ripple +
+

Whether the ripple effect is disabled or not.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the button is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ ClickEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a user click event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
is_target +
+

Whether the clicked target is the component which attached the event handler.

+
+

+ + TYPE: + bool + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/button_toggle/index.html b/components/button_toggle/index.html new file mode 100644 index 000000000..a9b12668c --- /dev/null +++ b/components/button_toggle/index.html @@ -0,0 +1,2871 @@ + + + + + + + + + + + + + + + + + + + + + + + Button toggle - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Button toggle

+ +

Overview

+

Button toggle is based on the Angular Material button toggle component.

+

Examples

+ + +
from dataclasses import field
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  selected_values: list[str] = field(
+    default_factory=lambda: ["bold", "underline"]
+  )
+
+
+def load(e: me.LoadEvent):
+  me.set_theme_mode("system")
+
+
+@me.page(on_load=load, path="/button_toggle")
+def app():
+  state = me.state(State)
+
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.button_toggle(
+      value=state.selected_values,
+      buttons=[
+        me.ButtonToggleButton(label="Bold", value="bold"),
+        me.ButtonToggleButton(label="Italic", value="italic"),
+        me.ButtonToggleButton(label="Underline", value="underline"),
+      ],
+      multiple=True,
+      hide_selection_indicator=False,
+      disabled=False,
+      on_change=on_change,
+      style=me.Style(margin=me.Margin(bottom=20)),
+    )
+
+    me.text("Select buttons: " + " ".join(state.selected_values))
+
+
+def on_change(e: me.ButtonToggleChangeEvent):
+  state = me.state(State)
+  state.selected_values = e.values
+
+

API

+ + +
+ + +

+ button_toggle + +

+ + +
+ +

This function creates a button toggle.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
value +
+

Selected values of the button toggle.

+
+

+ + TYPE: + list[str] | str + + + DEFAULT: + '' + +

+
buttons +
+

List of button toggles.

+
+

+ + TYPE: + Iterable[ButtonToggleButton] + +

+
on_change +
+

Event emitted when the group's value changes.

+
+

+ + TYPE: + Callable[[ButtonToggleChangeEvent], Any] | None + + + DEFAULT: + None + +

+
multiple +
+

Whether multiple button toggles can be selected.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether multiple button toggle group is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_selection_indicator +
+

Whether checkmark indicator for button toggle groups is hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ ButtonToggleButton + + + dataclass + + +

+ + +
+ + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
label +
+

Content to show for the button toggle button

+
+

+ + TYPE: + str | None + +

+
value +
+

The value of the button toggle button.

+
+

+ + TYPE: + str | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ ButtonToggleChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event representing a change in the button toggle component's selected values.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
values +
+

The new values of the button toggle component after the change.

+
+

+ + TYPE: + list[str] + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + +
+ + + +

+ value + + + property + + +

+ + +
+ +

Shortcut for returning a single value.

+
+ +
+ + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/chat/index.html b/components/chat/index.html new file mode 100644 index 000000000..3eab73358 --- /dev/null +++ b/components/chat/index.html @@ -0,0 +1,2533 @@ + + + + + + + + + + + + + + + + + + + + + + + Chat - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Chat

+ +

Overview

+

Chat component is a quick way to create a simple chat interface. Chat is part of Mesop Labs.

+

Examples

+ + +
import random
+import time
+
+import mesop as me
+import mesop.labs as mel
+
+
+def on_load(e: me.LoadEvent):
+  me.set_theme_mode("system")
+
+
+@me.page(
+  security_policy=me.SecurityPolicy(
+    allowed_iframe_parents=["https://google.github.io"]
+  ),
+  path="/chat",
+  title="Mesop Demo Chat",
+  on_load=on_load,
+)
+def page():
+  mel.chat(transform, title="Mesop Demo Chat", bot_user="Mesop Bot")
+
+
+def transform(input: str, history: list[mel.ChatMessage]):
+  for line in random.sample(LINES, random.randint(3, len(LINES) - 1)):
+    time.sleep(0.3)
+    yield line + " "
+
+
+LINES = [
+  "Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.",
+  "It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.",
+  "With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.",
+  "Deployment is straightforward, utilizing standard HTTP technologies.",
+  "Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.",
+  "It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.",
+  "Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.",
+]
+
+

API

+ + +
+ + +

+ chat + +

+ + +
+ +

Creates a simple chat UI which takes in a prompt and chat history and returns a +response to the prompt.

+

This function creates event handlers for text input and output operations +using the provided function transform to process the input and generate the output.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
transform +
+

Function that takes in a prompt and chat history and returns a response to the prompt.

+
+

+ + TYPE: + Callable[[str, list[ChatMessage]], Generator[str, None, None] | str] + +

+
title +
+

Headline text to display at the top of the UI.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
bot_user +
+

Name of your bot / assistant.

+
+

+ + TYPE: + str + + + DEFAULT: + _BOT_USER_DEFAULT + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/checkbox/index.html b/components/checkbox/index.html new file mode 100644 index 000000000..e3b0a33fa --- /dev/null +++ b/components/checkbox/index.html @@ -0,0 +1,3136 @@ + + + + + + + + + + + + + + + + + + + + + + + Checkbox - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Checkbox

+ +

Overview

+

Checkbox is a multi-selection form control and is based on the Angular Material checkbox component.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  checked: bool
+
+
+def on_update(event: me.CheckboxChangeEvent):
+  state = me.state(State)
+  state.checked = event.checked
+
+
+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="/checkbox",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    state = me.state(State)
+    me.checkbox(
+      "Simple checkbox",
+      on_change=on_update,
+    )
+
+    if state.checked:
+      me.text(text="is checked")
+    else:
+      me.text(text="is not checked")
+
+

API

+ + +
+ + +

+ checkbox + +

+ + +
+ +

Creates a simple Checkbox component with a text label.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Text label for checkbox

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
on_change +
+

Event emitted when the checkbox's checked value changes.

+
+

+ + TYPE: + Callable[[CheckboxChangeEvent], Any] | None + + + DEFAULT: + None + +

+
on_indeterminate_change +
+

Event emitted when the checkbox's indeterminate value changes.

+
+

+ + TYPE: + Callable[[CheckboxIndeterminateChangeEvent], Any] | None + + + DEFAULT: + None + +

+
label_position +
+

Whether the label should appear after or before the checkbox. Defaults to 'after'

+
+

+ + TYPE: + Literal['before', 'after'] + + + DEFAULT: + 'after' + +

+
disable_ripple +
+

Whether the checkbox has a ripple.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
tab_index +
+

Tabindex for the checkbox.

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
color +
+

Palette color of the checkbox.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
checked +
+

Whether the checkbox is checked.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the checkbox is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
indeterminate +
+

Whether the checkbox is indeterminate. This is also known as "mixed" mode and can be used to represent a checkbox with three states, e.g. a checkbox that represents a nested list of checkable items. Note that whenever checkbox is manually clicked, indeterminate is immediately set to false.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + +

+ content_checkbox + +

+ + +
+ +

Creates a Checkbox component which is a composite component. Typically, you would use a text or icon component as a child.

+

Intended for advanced use cases.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
on_change +
+

Event emitted when the checkbox's checked value changes.

+
+

+ + TYPE: + Callable[[CheckboxChangeEvent], Any] | None + + + DEFAULT: + None + +

+
on_indeterminate_change +
+

Event emitted when the checkbox's indeterminate value changes.

+
+

+ + TYPE: + Callable[[CheckboxIndeterminateChangeEvent], Any] | None + + + DEFAULT: + None + +

+
label_position +
+

Whether the label should appear after or before the checkbox. Defaults to 'after'

+
+

+ + TYPE: + Literal['before', 'after'] + + + DEFAULT: + 'after' + +

+
disable_ripple +
+

Whether the checkbox has a ripple.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
tab_index +
+

Tabindex for the checkbox.

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
color +
+

Palette color of the checkbox.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
checked +
+

Whether the checkbox is checked.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the checkbox is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
indeterminate +
+

Whether the checkbox is indeterminate. This is also known as "mixed" mode and can be used to represent a checkbox with three states, e.g. a checkbox that represents a nested list of checkable items. Note that whenever checkbox is manually clicked, indeterminate is immediately set to false.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ CheckboxChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a checkbox state change event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
checked +
+

The new checked state of the checkbox.

+
+

+ + TYPE: + bool + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ CheckboxIndeterminateChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a checkbox indeterminate state change event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
checked +
+

The new indeterminate state of the checkbox.

+
+

+

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/code/index.html b/components/code/index.html new file mode 100644 index 000000000..affef1c1a --- /dev/null +++ b/components/code/index.html @@ -0,0 +1,2461 @@ + + + + + + + + + + + + + + + + + + + + + + + Code - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Code

+ +

Overview

+

Code is used to render code with syntax highlighting. code is a simple wrapper around markdown.

+

Examples

+ + +
import inspect
+
+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="/code_demo",
+)
+def code_demo():
+  with me.box(
+    style=me.Style(
+      padding=me.Padding.all(15),
+      background=me.theme_var("surface-container-lowest"),
+    )
+  ):
+    me.text("Defaults to Python")
+    me.code("a = 123")
+
+    me.text("Can set to other languages")
+    me.code("<div class='a'>foo</div>", language="html")
+
+    me.text("Bigger code block")
+    me.code(inspect.getsource(me))
+
+

API

+ + +
+ + +

+ code + +

+ + +
+ +

Creates a code component which displays code with syntax highlighting.

+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/date_picker/index.html b/components/date_picker/index.html new file mode 100644 index 000000000..de3f5779f --- /dev/null +++ b/components/date_picker/index.html @@ -0,0 +1,2866 @@ + + + + + + + + + + + + + + + + + + + + + + + Date picker - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Date picker

+ +

Overview

+

Date picker allows the user to enter free text or select a date from a calendar widget. +and is based on the Angular Material datapicker component.

+

Examples

+ + +
from dataclasses import field
+from datetime import date
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  picked_date: date | None = field(default_factory=lambda: date(2024, 10, 1))
+
+
+def on_load(e: me.LoadEvent):
+  me.set_theme_mode("system")
+
+
+@me.page(path="/date_picker", on_load=on_load)
+def app():
+  state = me.state(State)
+  with me.box(
+    style=me.Style(
+      display="flex",
+      flex_direction="column",
+      gap=15,
+      padding=me.Padding.all(15),
+    )
+  ):
+    me.date_picker(
+      label="Date",
+      disabled=False,
+      placeholder="9/1/2024",
+      required=True,
+      value=state.picked_date,
+      readonly=False,
+      hide_required_marker=False,
+      color="accent",
+      float_label="always",
+      appearance="outline",
+      on_change=on_date_change,
+    )
+
+    me.text("Selected date: " + _render_date(state.picked_date))
+
+
+def on_date_change(e: me.DatePickerChangeEvent):
+  state = me.state(State)
+  state.picked_date = e.date
+
+
+def _render_date(maybe_date: date | None) -> str:
+  if maybe_date:
+    return maybe_date.strftime("%Y-%m-%d")
+  return "None"
+
+

API

+ + +
+ + +

+ date_picker + +

+ + +
+ +

Creates a date picker component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Label for date picker input.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
on_change +
+

Fires when a valid date value has been specified through Calendar date selection or user input blur.

+
+

+ + TYPE: + Callable[[DatePickerChangeEvent], Any] | None + + + DEFAULT: + None + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
style +
+

Style for date picker input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder +
+

Placeholder value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
required +
+

Whether it's required.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
value +
+

Initial value.

+
+

+ + TYPE: + date | None + + + DEFAULT: + None + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_required_marker +
+

Whether the required marker should be hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

The color palette for the form field.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
float_label +
+

Whether the label should always float or float as the user types.

+
+

+ + TYPE: + Literal['always', 'auto'] + + + DEFAULT: + 'auto' + +

+
subscript_sizing +
+

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

+
+

+ + TYPE: + Literal['fixed', 'dynamic'] + + + DEFAULT: + 'fixed' + +

+
hint_label +
+

Text for the form field hint.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ DatePickerChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a date picker change event.

+

This event will only fire if a valid date is specified.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
date +
+

Date value

+
+

+ + TYPE: + date + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/date_range_picker/index.html b/components/date_range_picker/index.html new file mode 100644 index 000000000..2f73c4e5f --- /dev/null +++ b/components/date_range_picker/index.html @@ -0,0 +1,2925 @@ + + + + + + + + + + + + + + + + + + + + + + + Date range picker - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Date range picker

+ +

Overview

+

Date range picker allows the user to enter free text or select a dates from a calendar widget. +and is based on the Angular Material datapicker component.

+

Examples

+ + +
from dataclasses import field
+from datetime import date
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  picked_start_date: date | None = field(
+    default_factory=lambda: date(2024, 10, 1)
+  )
+  picked_end_date: date | None = field(
+    default_factory=lambda: date(2024, 11, 1)
+  )
+
+
+def on_load(e: me.LoadEvent):
+  me.set_theme_mode("system")
+
+
+@me.page(path="/date_range_picker", on_load=on_load)
+def app():
+  state = me.state(State)
+  with me.box(
+    style=me.Style(
+      display="flex",
+      flex_direction="column",
+      gap=15,
+      padding=me.Padding.all(15),
+    )
+  ):
+    me.date_range_picker(
+      label="Date Range",
+      disabled=False,
+      placeholder_start_date="9/1/2024",
+      placeholder_end_date="10/1/2024",
+      required=True,
+      start_date=state.picked_start_date,
+      end_date=state.picked_end_date,
+      readonly=False,
+      hide_required_marker=False,
+      color="accent",
+      float_label="always",
+      appearance="outline",
+      on_change=on_date_range_change,
+    )
+
+    me.text("Start date: " + _render_date(state.picked_start_date))
+    me.text("End date: " + _render_date(state.picked_end_date))
+
+
+def on_date_range_change(e: me.DateRangePickerChangeEvent):
+  state = me.state(State)
+  state.picked_start_date = e.start_date
+  state.picked_end_date = e.end_date
+
+
+def _render_date(maybe_date: date | None) -> str:
+  if maybe_date:
+    return maybe_date.strftime("%Y-%m-%d")
+  return "None"
+
+

API

+ + +
+ + +

+ date_range_picker + +

+ + +
+ +

Creates a date range picker component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Label for date range picker input.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
on_change +
+

Fires when valid date values for both start/end have been specified through Calendar date selection or user input blur.

+
+

+ + TYPE: + Callable[[DateRangePickerChangeEvent], Any] | None + + + DEFAULT: + None + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
style +
+

Style for date range picker input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder_start_date +
+

Start date placeholder value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
placeholder_end_date +
+

End date placeholder value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
required +
+

Whether it's required.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
start_date +
+

Start date initial value.

+
+

+ + TYPE: + date | None + + + DEFAULT: + None + +

+
end_date +
+

End date initial value.

+
+

+ + TYPE: + date | None + + + DEFAULT: + None + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_required_marker +
+

Whether the required marker should be hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

The color palette for the form field.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
float_label +
+

Whether the label should always float or float as the user types.

+
+

+ + TYPE: + Literal['always', 'auto'] + + + DEFAULT: + 'auto' + +

+
subscript_sizing +
+

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

+
+

+ + TYPE: + Literal['fixed', 'dynamic'] + + + DEFAULT: + 'fixed' + +

+
hint_label +
+

Text for the form field hint.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ DateRangePickerChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a date range picker change event.

+

This event will only fire if start and end dates are valid dates.

+ + + + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
start_date +
+

Start date

+
+

+ + TYPE: + date + +

+
end_date +
+

End date

+
+

+ + TYPE: + date + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/divider/index.html b/components/divider/index.html new file mode 100644 index 000000000..1f5c33b46 --- /dev/null +++ b/components/divider/index.html @@ -0,0 +1,2497 @@ + + + + + + + + + + + + + + + + + + + + + + + Divider - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Divider

+ +

Overview

+

Divider is used to provide visual separation and is based on the Angular Material divider component.

+

Examples

+ + +
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="/divider",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text(text="before")
+    me.divider()
+    me.text(text="after")
+
+

API

+ + +
+ + +

+ divider + +

+ + +
+ +

Creates a Divider component.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
inset +
+

Whether the divider is an inset divider.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/embed/index.html b/components/embed/index.html new file mode 100644 index 000000000..98ecdee35 --- /dev/null +++ b/components/embed/index.html @@ -0,0 +1,2513 @@ + + + + + + + + + + + + + + + + + + + + + + + Embed - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Embed

+ +

Overview

+

Embed allows you to embed/iframe another web site in your Mesop app.

+

Examples

+ + +
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="/embed",
+)
+def app():
+  src = "https://google.github.io/mesop/"
+  me.text("Embedding: " + src, style=me.Style(padding=me.Padding.all(15)))
+  me.embed(
+    src=src,
+    style=me.Style(width="100%", height="100%"),
+  )
+
+

API

+ + +
+ + +

+ embed + +

+ + +
+ +

This function creates an embed component.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
src +
+

The source URL for the embed content.

+
+

+ + TYPE: + str + +

+
style +
+

The style to apply to the embed, such as width and height.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/html/index.html b/components/html/index.html new file mode 100644 index 000000000..f90a09311 --- /dev/null +++ b/components/html/index.html @@ -0,0 +1,2554 @@ + + + + + + + + + + + + + + + + + + + + + + + HTML - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

HTML

+ +

Overview

+

The HTML component allows you to add custom HTML to your Mesop app.

+

There are two modes for rendering HTML components:

+
    +
  • sanitized (default), where the HTML is sanitized by Angular to remove potentially unsafe code like <script> and <style> for web security reasons.
  • +
  • sandboxed, which allows rendering of <script> and <style> HTML content by using an iframe sandbox.
  • +
+

Examples

+ + +
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="/html_demo",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text("Sanitized HTML", type="headline-5")
+    me.html(
+      """
+  Custom HTML
+  <a href="https://google.github.io/mesop/" target="_blank">mesop</a>
+  """,
+      mode="sanitized",
+    )
+
+    with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=24))):
+      me.divider()
+
+    me.text("Sandboxed HTML", type="headline-5")
+    me.html(
+      "<style>body { color: #ff0000; }</style>hi<script>document.body.innerHTML = 'iamsandboxed'; </script>",
+      mode="sandboxed",
+    )
+
+

API

+ + +
+ + +

+ html + +

+ + +
+ +

This function renders custom HTML in a secure way.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
html +
+

The HTML content to be rendered.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
mode +
+

Determines how the HTML is rendered. Mode can be either "sanitized" or "sandboxed". +If "sanitized" then potentially dangerous content like <script> and <style> are +stripped out. If "sandboxed", then all content is allowed, but rendered in an iframe for isolation.

+
+

+ + TYPE: + Literal['sanitized', 'sandboxed'] | None + + + DEFAULT: + None + +

+
style +
+

The style to apply to the embed, such as width and height.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/icon/index.html b/components/icon/index.html new file mode 100644 index 000000000..7468e5d62 --- /dev/null +++ b/components/icon/index.html @@ -0,0 +1,2514 @@ + + + + + + + + + + + + + + + + + + + + + + + Icon - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Icon

+ +

Overview

+

Icon displays a Material icon/symbol and is based on the Angular Material icon component.

+

Examples

+ + +
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="/icon",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text("home icon")
+    me.icon(icon="home")
+
+

API

+ + +
+ + +

+ icon + +

+ + +
+ +

Creates a Icon component.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
icon +
+

Name of the Material Symbols icon.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
style +
+

Inline styles

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/image/index.html b/components/image/index.html new file mode 100644 index 000000000..c8fe8069c --- /dev/null +++ b/components/image/index.html @@ -0,0 +1,2535 @@ + + + + + + + + + + + + + + + + + + + + + + + Image - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Image

+ +

Overview

+

Image is the equivalent of an <img> HTML element.

+

Examples

+ + +
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="/image",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.image(
+      src="https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg",
+      alt="Grapefruit",
+      style=me.Style(width="100%"),
+    )
+
+

API

+ + +
+ + +

+ image + +

+ + +
+ +

This function creates an image component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
src +
+

The source URL of the image.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
alt +
+

The alternative text for the image if it cannot be displayed.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
style +
+

The style to apply to the image, such as width and height.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/index.html b/components/index.html new file mode 100644 index 000000000..cacde093a --- /dev/null +++ b/components/index.html @@ -0,0 +1,2579 @@ + + + + + + + + + + + + + + + + + + + + + + + Overview - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Components

+

Please read Core Concepts before this as it explains the basics of components. This page provides an overview of the different types of components in Mesop.

+

Types of components

+

Native components

+

Native components are components implemented using Angular/Javascript. Many of these components wrap Angular Material components. Other components are simple wrappers around DOM elements.

+

If you have a use case that's not supported by the existing native components, please file an issue on GitHub to explain your use case. Given our limited bandwidth, we may not be able to build it soon, but in the future, we will enable Mesop developers to build their own custom native components.

+

User-defined components

+

User-defined components are essentially Python functions which call other components, which can be native components or other user-defined components. It's very easy to write your own components, and it's encouraged to split your app into modular components for better maintainability and reusability.

+

Web components

+

Web components in Mesop are custom HTML elements created using JavaScript and CSS. They enable custom JavaScript execution and bi-directional communication between the browser and server. They can wrap JavaScript libraries and provide stateful client-side interactions. Learn more about web components.

+

Content components

+

Content components allow you to compose components more flexibly than regular components by accepting child(ren) components. A commonly used content component is the button component, which accepts a child component which oftentimes the text component.

+

Example:

+
with me.button():
+  me.text("Child")
+
+

You can also have multiple content components nested:

+
with me.box():
+  with me.box():
+    me.text("Grand-child")
+
+

Sometimes, you may want to define your own content component for better reusability. For example, let's say I want to define a scaffold component which includes a menu positioned on the left and a main content area, I could do the following:

+
@me.content_component
+def scaffold(url: str):
+  with me.box(style=me.Style(background="white")):
+    menu(url=url)
+    with me.box(style=me.Style(padding=me.Padding(left=MENU_WIDTH))):
+      me.slot()
+
+

Now other components can re-use this scaffold component:

+
def page1():
+  with scaffold(url="/page1"):
+    some_content(...)
+
+

This is similar to Angular's Content Projection.

+

Component Key

+

Every native component in Mesop accepts a key argument which is a component identifier. This is used by Mesop to tell Angular whether to reuse the DOM element.

+

Resetting a component

+

You can reset a component to the initial state (e.g. reset a select component to the unselected state) by giving it a new key value across renders.

+

For example, you can reset a component by "incrementing" the key:

+
class State:
+  select_menu_key: int
+
+def reset(event):
+  state = me.state(State)
+  state.select_menu_key += 1
+
+def main():
+  state = me.state(State)
+  me.select(key=str(state.select_menu_key),
+            options=[me.SelectOption(label="o1", value="o1")])
+  me.button(label="Reset", on_click=reset)
+
+

Event handlers

+

Every Mesop event includes the key of the component which emitted the event. This makes it useful when you want to reuse an event handler for multiple instances of a component:

+
def buttons():
+  for fruit in ["Apple", "Banana"]:
+    me.button(fruit, key=fruit, on_click=on_click)
+
+def on_click(event: me.ClickEvent):
+  fruit = event.key
+  print("fruit name", fruit)
+
+

Because a key is a str type, you may sometimes want to store more complex data like a dataclass or a proto object for retrieval in the event handler. To do this, you can serialize and deserialize:

+
import json
+from dataclasses import dataclass
+
+@dataclass
+class Person:
+  name: str
+
+def buttons():
+  for person in [Person(name="Alice"), Person(name="Bob")]:
+    # serialize dataclass into str
+    key = json.dumps(person.asdict())
+    me.button(person.name, key=key, on_click=on_click)
+
+def on_click(event: me.ClickEvent):
+  person_dict = json.loads(event.key)
+  # modify this for more complex deserialization
+  person = Person(**person_dict)
+
+
+

Use component key for reusable event handler

+

This avoids a subtle issue with using closure variables in event handlers.

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/input/index.html b/components/input/index.html new file mode 100644 index 000000000..aad33b555 --- /dev/null +++ b/components/input/index.html @@ -0,0 +1,3096 @@ + + + + + + + + + + + + + + + + + + + + + + + Input - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Input

+ +

Overview

+

Input allows the user to type in a value and is based on the Angular Material input component.

+

For longer text inputs, also see Textarea

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  input: str = ""
+
+
+def on_blur(e: me.InputBlurEvent):
+  state = me.state(State)
+  state.input = e.value
+
+
+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="/input",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    s = me.state(State)
+    me.input(label="Basic input", appearance="outline", on_blur=on_blur)
+    me.text(text=s.input)
+
+

API

+ + +
+ + +

+ input + +

+ + +
+ +

Creates a Input component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Label for input.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
on_blur +
+

blur is fired when the input has lost focus.

+
+

+ + TYPE: + Callable[[InputBlurEvent], Any] | None + + + DEFAULT: + None + +

+
on_input +
+

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

+
+

+ + TYPE: + Callable[[InputEvent], Any] | None + + + DEFAULT: + None + +

+
on_enter +
+

triggers when the browser detects an "Enter" key on a keyup native browser event.

+
+

+ + TYPE: + Callable[[InputEnterEvent], Any] | None + + + DEFAULT: + None + +

+
type +
+

Input type of the element. For textarea, use me.textarea(...)

+
+

+ + TYPE: + Literal['color', 'date', 'datetime-local', 'email', 'month', 'number', 'password', 'search', 'tel', 'text', 'time', 'url', 'week'] | None + + + DEFAULT: + None + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
style +
+

Style for input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder +
+

Placeholder value

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
required +
+

Whether it's required

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
value +
+

Initial value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_required_marker +
+

Whether the required marker should be hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

The color palette for the form field.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
float_label +
+

Whether the label should always float or float as the user types.

+
+

+ + TYPE: + Literal['always', 'auto'] + + + DEFAULT: + 'auto' + +

+
subscript_sizing +
+

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

+
+

+ + TYPE: + Literal['fixed', 'dynamic'] + + + DEFAULT: + 'fixed' + +

+
hint_label +
+

Text for the form field hint.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ InputBlurEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents an inpur blur event (when a user loses focus of an input).

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ InputEnterEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents an "Enter" keyboard event on an input component.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ InputEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a user input event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/link/index.html b/components/link/index.html new file mode 100644 index 000000000..59df6e81c --- /dev/null +++ b/components/link/index.html @@ -0,0 +1,2560 @@ + + + + + + + + + + + + + + + + + + + + + + + Link - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Link

+ +

Overview

+

Link creates an HTML anchor element (i.e. <a>) which links to another page.

+

Examples

+ + +
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="/link",
+)
+def link():
+  with me.box(
+    style=me.Style(
+      margin=me.Margin.all(15), display="flex", flex_direction="column", gap=10
+    )
+  ):
+    me.link(
+      text="Open in same tab",
+      url="https://google.github.io/mesop/",
+      style=me.Style(color=me.theme_var("primary")),
+    )
+    me.link(
+      text="Open in new tab",
+      open_in_new_tab=True,
+      url="https://google.github.io/mesop/",
+      style=me.Style(color=me.theme_var("primary")),
+    )
+    me.link(
+      text="Styled link",
+      url="https://google.github.io/mesop/",
+      style=me.Style(color=me.theme_var("tertiary"), text_decoration="none"),
+    )
+
+

API

+ + +
+ + + + + +
+ +

This function creates a link.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
text +
+

The text to be displayed.

+
+

+ + TYPE: + str + +

+
url +
+

The URL to navigate to.

+
+

+ + TYPE: + str + +

+
open_in_new_tab +
+

If True, open page in new tab. If False, open page in current tab.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component. Defaults to None.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

Unique key for the component. Defaults to None.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/markdown/index.html b/components/markdown/index.html new file mode 100644 index 000000000..6ebd8b50d --- /dev/null +++ b/components/markdown/index.html @@ -0,0 +1,2592 @@ + + + + + + + + + + + + + + + + + + + + + + + Markdown - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Markdown

+ +

Overview

+

Markdown is used to render markdown text.

+

Examples

+ + +
import mesop as me
+
+SAMPLE_MARKDOWN = """
+# Sample Markdown Document
+
+## Table of Contents
+1. [Headers](#headers)
+2. [Emphasis](#emphasis)
+3. [Lists](#lists)
+4. [Links](#links)
+5. [Code](#code)
+6. [Blockquotes](#blockquotes)
+7. [Tables](#tables)
+8. [Horizontal Rules](#horizontal-rules)
+
+## Headers
+# Header 1
+## Header 2
+### Header 3
+#### Header 4
+##### Header 5
+###### Header 6
+
+## Emphasis
+*Italic text* or _Italic text_
+**Bold text** or __Bold text__
+***Bold and Italic*** or ___Bold and Italic___
+
+## Lists
+
+### Unordered List
+- Item 1
+- Item 2
+    - Subitem 2.1
+    - Subitem 2.2
+
+### Ordered List
+1. First item
+2. Second item
+    1. Subitem 2.1
+    2. Subitem 2.2
+
+## Links
+[Google](https://www.google.com/)
+
+## Inline Code
+
+Inline `code`
+
+## Code
+
+```python
+import mesop as me
+
+
+@me.page(path="/hello_world")
+def app():
+  me.text("Hello World")
+
+

Table

+ + + + + + + + + + + + + + + + + + + + + +
First HeaderSecond Header
Content CellContent Cell
Content CellContent Cell
"""
+

def on_load(e: me.LoadEvent): + me.set_theme_mode("system")

+

@me.page( + security_policy=me.SecurityPolicy( + allowed_iframe_parents=["https://google.github.io"] + ), + path="/markdown_demo", + on_load=on_load, +) +def app(): + with me.box( + style=me.Style(background=me.theme_var("surface-container-lowest")) + ): + me.markdown(SAMPLE_MARKDOWN, style=me.Style(margin=me.Margin.all(15))) +```

+

API

+ + +
+ + +

+ markdown + +

+ + +
+ +

This function creates a markdown.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
text +
+

Required. Markdown text

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
style +
+

Style to apply to component. Follows HTML Element inline style API.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/plot/index.html b/components/plot/index.html new file mode 100644 index 000000000..c254a23aa --- /dev/null +++ b/components/plot/index.html @@ -0,0 +1,2500 @@ + + + + + + + + + + + + + + + + + + + + + + + Plot - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Plot

+ +

Overview

+

Plot provides a convenient way to render Matplotlib figures as an image.

+

Examples

+ + +
from matplotlib.figure import Figure
+
+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="/plot",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    # Create matplotlib figure without using pyplot:
+    fig = Figure()
+    ax = fig.subplots()  # type: ignore
+    ax.plot([1, 2])  # type: ignore
+
+    me.text("Example using matplotlib:", type="headline-5")
+    me.plot(fig, style=me.Style(width="100%"))
+
+

API

+ + +
+ + +

+ plot + +

+ + +
+ +

Creates a plot component from a Matplotlib figure.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
figure +
+

A Matplotlib figure which will be rendered.

+
+

+ + TYPE: + Figure + +

+
style +
+

An optional Style object that defines the visual styling for the +plot component. If None, default styling (e.g. height, width) is used.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/progress-bar/index.html b/components/progress-bar/index.html new file mode 100644 index 000000000..4a33318b2 --- /dev/null +++ b/components/progress-bar/index.html @@ -0,0 +1,2668 @@ + + + + + + + + + + + + + + + + + + + + + + + Progress bar - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Progress bar

+ +

Overview

+

Progress Bar is used to indicate something is in progress and is based on the Angular Material progress bar component.

+

Examples

+ + +
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="/progress_bar",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text("Default progress bar", type="headline-5")
+    me.progress_bar()
+
+

API

+ + +
+ + +

+ progress_bar + +

+ + +
+ +

Creates a Progress bar component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
color +
+

Theme palette color of the progress bar.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
value +
+

Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow.

+
+

+ + TYPE: + float + + + DEFAULT: + 0 + +

+
buffer_value +
+

Buffer value of the progress bar. Defaults to zero.

+
+

+ + TYPE: + float + + + DEFAULT: + 0 + +

+
mode +
+

Mode of the progress bar. Input must be one of these values: determinate, indeterminate, buffer, query, defaults to 'determinate'. Mirrored to mode attribute.

+
+

+ + TYPE: + Literal['determinate', 'indeterminate', 'buffer', 'query'] + + + DEFAULT: + 'indeterminate' + +

+
on_animation_end +
+

Event emitted when animation of the primary progress bar completes. This event will not be emitted when animations are disabled, nor will it be emitted for modes with continuous animations (indeterminate and query).

+
+

+ + TYPE: + Callable[[ProgressBarAnimationEndEvent], Any] | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ ProgressBarAnimationEndEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event emitted when the animation of the progress bar ends.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

The value of the progress bar when the animation ends.

+
+

+ + TYPE: + float + +

+
key +
+

Key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/progress-spinner/index.html b/components/progress-spinner/index.html new file mode 100644 index 000000000..9d376a062 --- /dev/null +++ b/components/progress-spinner/index.html @@ -0,0 +1,2531 @@ + + + + + + + + + + + + + + + + + + + + + + + Progress spinner - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Progress spinner

+ +

Overview

+

Progress Spinner is used to indicate something is in progress and is based on the Angular Material progress spinner component.

+

Examples

+ + +
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="/progress_spinner",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.progress_spinner()
+
+

API

+ + +
+ + +

+ progress_spinner + +

+ + +
+ +

Creates a Progress spinner component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
color +
+

Theme palette color of the progress spinner.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
diameter +
+

The diameter of the progress spinner (will set width and height of svg).

+
+

+ + TYPE: + float + + + DEFAULT: + 48 + +

+
stroke_width +
+

Stroke width of the progress spinner.

+
+

+ + TYPE: + float + + + DEFAULT: + 4 + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/radio/index.html b/components/radio/index.html new file mode 100644 index 000000000..80fc8432f --- /dev/null +++ b/components/radio/index.html @@ -0,0 +1,2820 @@ + + + + + + + + + + + + + + + + + + + + + + + Radio - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Radio

+ +

Overview

+

Radio is a single selection form control based on the Angular Material radio component.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  radio_value: str = "2"
+
+
+def on_change(event: me.RadioChangeEvent):
+  s = me.state(State)
+  s.radio_value = event.value
+
+
+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="/radio",
+)
+def app():
+  s = me.state(State)
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text("Horizontal radio options")
+    me.radio(
+      on_change=on_change,
+      options=[
+        me.RadioOption(label="Option 1", value="1"),
+        me.RadioOption(label="Option 2", value="2"),
+      ],
+      value=s.radio_value,
+    )
+    me.text(text="Selected radio value: " + s.radio_value)
+
+

API

+ + +
+ + +

+ radio + +

+ + +
+ +

Creates a Radio component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
options +
+

List of radio options

+
+

+ + TYPE: + Iterable[RadioOption] + + + DEFAULT: + () + +

+
on_change +
+

Event emitted when the group value changes. Change events are only emitted when the value changes due to user interaction with a radio button (the same behavior as <input type-"radio">).

+
+

+ + TYPE: + Callable[[RadioChangeEvent], Any] | None + + + DEFAULT: + None + +

+
color +
+

Theme color for all of the radio buttons in the group.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
label_position +
+

Whether the labels should appear after or before the radio-buttons. Defaults to 'after'

+
+

+ + TYPE: + Literal['before', 'after'] + + + DEFAULT: + 'after' + +

+
value +
+

Value for the radio-group. Should equal the value of the selected radio button if there is a corresponding radio button with a matching value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
disabled +
+

Whether the radio group is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ RadioOption + + + dataclass + + +

+ + +
+ + + + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
label +
+

Content to show for the radio option

+
+

+ + TYPE: + str | None + +

+
value +
+

The value of this radio button.

+
+

+ + TYPE: + str | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ RadioChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event representing a change in the radio component's value.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

The new value of the radio component after the change.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/select/index.html b/components/select/index.html new file mode 100644 index 000000000..e4f55eac1 --- /dev/null +++ b/components/select/index.html @@ -0,0 +1,3052 @@ + + + + + + + + + + + + + + + + + + + + + + + Select - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Select

+ +

Overview

+

Select allows the user to choose from a list of values and is based on the Angular Material select component.

+

Examples

+ + +
from dataclasses import field
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  selected_values: list[str] = field(
+    default_factory=lambda: ["value1", "value3"]
+  )
+
+
+def on_selection_change(e: me.SelectSelectionChangeEvent):
+  s = me.state(State)
+  s.selected_values = e.values
+
+
+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="/select_demo",
+)
+def app():
+  state = me.state(State)
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.select(
+      label="Select",
+      options=[
+        me.SelectOption(label="label 1", value="value1"),
+        me.SelectOption(label="label 2", value="value2"),
+        me.SelectOption(label="label 3", value="value3"),
+      ],
+      on_selection_change=on_selection_change,
+      style=me.Style(width=500),
+      multiple=True,
+      appearance="outline",
+      value=state.selected_values,
+    )
+    me.text(text="Selected values: " + ", ".join(state.selected_values))
+
+

API

+ + +
+ + +

+ select + +

+ + +
+ +

Creates a Select component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
options +
+

List of select options.

+
+

+ + TYPE: + Iterable[SelectOption] + + + DEFAULT: + () + +

+
on_selection_change +
+

Event emitted when the selected value has been changed by the user.

+
+

+ + TYPE: + Callable[[SelectSelectionChangeEvent], Any] | None + + + DEFAULT: + None + +

+
on_opened_change +
+

Event emitted when the select panel has been toggled.

+
+

+ + TYPE: + Callable[[SelectOpenedChangeEvent], Any] | None + + + DEFAULT: + None + +

+
disabled +
+

Whether the select is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disable_ripple +
+

Whether ripples in the select are disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
multiple +
+

Whether multiple selections are allowed.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
tab_index +
+

Tab index of the select.

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
placeholder +
+

Placeholder to be shown if no value has been selected.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
value +
+

Value(s) of the select control.

+
+

+ + TYPE: + list[str] | str + + + DEFAULT: + '' + +

+
style +
+

Style.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ SelectOption + + + dataclass + + +

+ + +
+ + +

Represents an option within a select component.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
label +
+

The content shown for the select option.

+
+

+ + TYPE: + str | None + +

+
value +
+

The value associated with the select option.

+
+

+ + TYPE: + str | None + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ SelectSelectionChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event representing a change in the select component's value(s).

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
values +
+

New values of the select component after the change.

+
+

+ + TYPE: + list[str] + +

+
key +
+

Key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + +
+ + + +

+ value + + + property + + +

+ + +
+ +

Shortcut for returning a single value.

+
+ +
+ + + + + +
+ +
+ +
+ +
+ + + +

+ SelectOpenedChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event representing the opened state change of the select component.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
opened +
+

A boolean indicating whether the select component is opened (True) or closed (False).

+
+

+ + TYPE: + bool + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/sidenav/index.html b/components/sidenav/index.html new file mode 100644 index 000000000..edf017f25 --- /dev/null +++ b/components/sidenav/index.html @@ -0,0 +1,2605 @@ + + + + + + + + + + + + + + + + + + + + + + + Sidenav - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Sidenav

+ +

Overview

+

Sidenav is a sidebar typically used for navigation and is based on the Angular Material sidenav component.

+

Examples

+ + +
import mesop as me
+
+SIDENAV_WIDTH = 200
+
+
+@me.stateclass
+class State:
+  sidenav_open: bool
+
+
+def on_click(e: me.ClickEvent):
+  s = me.state(State)
+  s.sidenav_open = not s.sidenav_open
+
+
+def opened_changed(e: me.SidenavOpenedChangedEvent):
+  s = me.state(State)
+  s.sidenav_open = e.opened
+
+
+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="/sidenav",
+)
+def app():
+  state = me.state(State)
+  with me.sidenav(
+    opened=state.sidenav_open,
+    disable_close=False,
+    on_opened_changed=opened_changed,
+    style=me.Style(
+      border_radius=0,
+      width=SIDENAV_WIDTH,
+      background=me.theme_var("surface-container-low"),
+      padding=me.Padding.all(15),
+    ),
+  ):
+    me.text("Inside sidenav")
+
+  with me.box(
+    style=me.Style(
+      margin=me.Margin(left=SIDENAV_WIDTH if state.sidenav_open else 0),
+      padding=me.Padding.all(15),
+    ),
+  ):
+    with me.content_button(on_click=on_click):
+      me.icon("menu")
+    me.markdown("Main content")
+
+

API

+ + +
+ + +

+ sidenav + +

+ + +
+ +

This function creates a sidenav.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
opened +
+

A flag to determine if the sidenav is open or closed. Defaults to True.

+
+

+ + TYPE: + bool + + + DEFAULT: + True + +

+
disable_close +
+

Whether the drawer can be closed with the escape key.

+
+

+ + TYPE: + bool + + + DEFAULT: + True + +

+
position +
+

The side that the drawer is attached to.

+
+

+ + TYPE: + Literal['start', 'end'] + + + DEFAULT: + 'start' + +

+
on_opened_changed +
+

Handles event emitted when the drawer open state is changed.

+
+

+ + TYPE: + Callable[[SidenavOpenedChangedEvent], Any] | None + + + DEFAULT: + None + +

+
style +
+

An optional Style object to apply custom styles. Defaults to None.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/slide-toggle/index.html b/components/slide-toggle/index.html new file mode 100644 index 000000000..281813dec --- /dev/null +++ b/components/slide-toggle/index.html @@ -0,0 +1,2983 @@ + + + + + + + + + + + + + + + + + + + + + + + Slide toggle - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Slide toggle

+ +

Overview

+

Slide Toggle allows the user to toggle on and off and is based on the Angular Material slide toggle component.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  toggled: bool = False
+
+
+def on_change(event: me.SlideToggleChangeEvent):
+  s = me.state(State)
+  s.toggled = not s.toggled
+
+
+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="/slide_toggle",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.slide_toggle(label="Slide toggle", on_change=on_change)
+    s = me.state(State)
+    me.text(text=f"Toggled: {s.toggled}")
+
+

API

+ + +
+ + +

+ slide_toggle + +

+ + +
+ +

Creates a simple Slide toggle component with a text label.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Text label for slide toggle

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
on_change +
+

An event will be dispatched each time the slide-toggle changes its value.

+
+

+ + TYPE: + Callable[[SlideToggleChangeEvent], Any] | None + + + DEFAULT: + None + +

+
label_position +
+

Whether the label should appear after or before the slide-toggle. Defaults to 'after'.

+
+

+ + TYPE: + Literal['before', 'after'] + + + DEFAULT: + 'after' + +

+
required +
+

Whether the slide-toggle is required.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

Palette color of slide toggle.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
disabled +
+

Whether the slide toggle is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disable_ripple +
+

Whether the slide toggle has a ripple.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
tab_index +
+

Tabindex of slide toggle.

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
checked +
+

Whether the slide-toggle element is checked or not.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_icon +
+

Whether to hide the icon inside of the slide toggle.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + +

+ content_slide_toggle + +

+ + +
+ +

Creates a Slide toggle component which is a composite component. Typically, you would use a text or icon component as a child.

+

Intended for advanced use cases.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
on_change +
+

An event will be dispatched each time the slide-toggle changes its value.

+
+

+ + TYPE: + Callable[[SlideToggleChangeEvent], Any] | None + + + DEFAULT: + None + +

+
label_position +
+

Whether the label should appear after or before the slide-toggle. Defaults to 'after'.

+
+

+ + TYPE: + Literal['before', 'after'] + + + DEFAULT: + 'after' + +

+
required +
+

Whether the slide-toggle is required.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

Palette color of slide toggle.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
disabled +
+

Whether the slide toggle is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disable_ripple +
+

Whether the slide toggle has a ripple.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
tab_index +
+

Tabindex of slide toggle.

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
checked +
+

Whether the slide-toggle element is checked or not.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_icon +
+

Whether to hide the icon inside of the slide toggle.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ SlideToggleChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event triggered when the slide toggle state changes.

+ + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
key +
+

Key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/slider/index.html b/components/slider/index.html new file mode 100644 index 000000000..3a4178499 --- /dev/null +++ b/components/slider/index.html @@ -0,0 +1,2806 @@ + + + + + + + + + + + + + + + + + + + + + + + Slider - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Slider

+ +

Overview

+

Slider allows the user to select from a range of values and is based on the Angular Material slider component.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  initial_input_value: str = "50.0"
+  initial_slider_value: float = 50.0
+  slider_value: float = 50.0
+
+
+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="/slider",
+)
+def app():
+  state = me.state(State)
+  with me.box(
+    style=me.Style(
+      display="flex", flex_direction="column", margin=me.Margin.all(15)
+    )
+  ):
+    me.input(
+      label="Slider value",
+      appearance="outline",
+      value=state.initial_input_value,
+      on_input=on_input,
+    )
+    me.slider(on_value_change=on_value_change, value=state.initial_slider_value)
+    me.text(text=f"Value: {me.state(State).slider_value}")
+
+
+def on_value_change(event: me.SliderValueChangeEvent):
+  state = me.state(State)
+  state.slider_value = event.value
+  state.initial_input_value = str(state.slider_value)
+
+
+def on_input(event: me.InputEvent):
+  state = me.state(State)
+  state.initial_slider_value = float(event.value)
+  state.slider_value = state.initial_slider_value
+
+

API

+ + +
+ + +

+ slider + +

+ + +
+ +

Creates a Slider component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
on_value_change +
+

An event will be dispatched each time the slider changes its value.

+
+

+ + TYPE: + Callable[[SliderValueChangeEvent], Any] | None + + + DEFAULT: + None + +

+
value +
+

Initial value. If updated, the slider will be updated with a new initial value.

+
+

+ + TYPE: + float | None + + + DEFAULT: + None + +

+
min +
+

The minimum value that the slider can have.

+
+

+ + TYPE: + float + + + DEFAULT: + 0 + +

+
max +
+

The maximum value that the slider can have.

+
+

+ + TYPE: + float + + + DEFAULT: + 100 + +

+
step +
+

The values at which the thumb will snap.

+
+

+ + TYPE: + float + + + DEFAULT: + 1 + +

+
disabled +
+

Whether the slider is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
discrete +
+

Whether the slider displays a numeric value label upon pressing the thumb.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
show_tick_marks +
+

Whether the slider displays tick marks along the slider track.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

Palette color of the slider.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
disable_ripple +
+

Whether ripples are disabled in the slider.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ SliderValueChangeEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event triggered when the slider value changes.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

The new value of the slider after the change.

+
+

+ + TYPE: + float + +

+
key +
+

Key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/table/index.html b/components/table/index.html new file mode 100644 index 000000000..290117a1f --- /dev/null +++ b/components/table/index.html @@ -0,0 +1,2581 @@ + + + + + + + + + + + + + + + + + + + + + + + Table - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Table

+ +

Overview

+

Table allows the user to render an Angular Material table component from a Pandas data frame.

+

Examples

+ + +
from datetime import datetime
+
+import numpy as np
+import pandas as pd
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  selected_cell: str = "No cell selected."
+
+
+df = pd.DataFrame(
+  data={
+    "NA": [pd.NA, pd.NA, pd.NA],
+    "Index": [3, 2, 1],
+    "Bools": [True, False, np.bool_(True)],
+    "Ints": [101, 90, np.int64(-55)],
+    "Floats": [2.3, 4.5, np.float64(-3.000000003)],
+    "Strings": ["Hello", "World", "!"],
+    "Date Times": [
+      pd.Timestamp("20180310"),
+      pd.Timestamp("20230310"),
+      datetime(2023, 1, 1, 12, 12, 1),
+    ],
+  }
+)
+
+
+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="/table",
+)
+def app():
+  state = me.state(State)
+
+  with me.box(style=me.Style(padding=me.Padding.all(10), width=500)):
+    me.table(
+      df,
+      on_click=on_click,
+      header=me.TableHeader(sticky=True),
+      columns={
+        "NA": me.TableColumn(sticky=True),
+        "Index": me.TableColumn(sticky=True),
+      },
+    )
+
+  with me.box(
+    style=me.Style(
+      background=me.theme_var("surface-container-high"),
+      margin=me.Margin.all(10),
+      padding=me.Padding.all(10),
+    )
+  ):
+    me.text(state.selected_cell)
+
+
+def on_click(e: me.TableClickEvent):
+  state = me.state(State)
+  state.selected_cell = (
+    f"Selected cell at col {e.col_index} and row {e.row_index} "
+    f"with value {df.iat[e.row_index, e.col_index]!s}"
+  )
+
+

API

+ + +
+ + +

+ table + +

+ + +
+ +

This function creates a table from Pandas data frame

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
data_frame +
+

Pandas data frame.

+
+

+ + TYPE: + Any + +

+
on_click +
+

Triggered when a table cell is clicked. The click event is a native browser event.

+
+

+ + TYPE: + Callable[[TableClickEvent], Any] | None + + + DEFAULT: + None + +

+
header +
+

Configures table header to be sticky or not.

+
+

+ + TYPE: + TableHeader | None + + + DEFAULT: + None + +

+
columns +
+

Configures table columns to be sticky or not. The key is the name of the column.

+
+

+ + TYPE: + dict[str, TableColumn] | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/text-to-image/index.html b/components/text-to-image/index.html new file mode 100644 index 000000000..e8cf64e2c --- /dev/null +++ b/components/text-to-image/index.html @@ -0,0 +1,2502 @@ + + + + + + + + + + + + + + + + + + + + + + + Text to Image - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Text to Image

+ +

Overview

+

Text To Image component is a quick and simple way of getting started with Mesop. Text To Image is part of Mesop Labs.

+

Examples

+ + +
import mesop as me
+import mesop.labs as mel
+
+
+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="/text_to_image",
+  title="Text to Image Example",
+)
+def app():
+  mel.text_to_image(
+    generate_image,
+    title="Text to Image Example",
+  )
+
+
+def generate_image(prompt: str):
+  return "https://www.google.com/logos/doodles/2024/earth-day-2024-6753651837110453-2xa.gif"
+
+

API

+ + +
+ + +

+ text_to_image + +

+ + +
+ +

Creates a simple UI which takes in a text input and returns an image output.

+

This function creates event handlers for text input and output operations +using the provided function transform to process the input and generate the image +output.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
transform +
+

Function that takes in a string input and returns a URL to an image or a base64 encoded image.

+
+

+ + TYPE: + Callable[[str], str] + +

+
title +
+

Headline text to display at the top of the UI.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/text-to-text/index.html b/components/text-to-text/index.html new file mode 100644 index 000000000..eca5f9896 --- /dev/null +++ b/components/text-to-text/index.html @@ -0,0 +1,2623 @@ + + + + + + + + + + + + + + + + + + + + + + + Text to Text - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Text to Text

+ +

Overview

+

Text to text component allows you to take in user inputted text and return a transformed text. This is part of Mesop Labs.

+

Examples

+ + +
import mesop as me
+import mesop.labs as mel
+
+
+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="/text_to_text",
+  title="Text to Text Example",
+)
+def app():
+  mel.text_to_text(
+    upper_case_stream,
+    title="Text to Text Example",
+  )
+
+
+def upper_case_stream(s: str):
+  return "Echo: " + s
+
+

API

+ + +
+ + +

+ text_to_text + +

+ + +
+ +

Creates a simple UI which takes in a text input and returns a text output.

+

This function creates event handlers for text input and output operations +using the provided transform function to process the input and generate the output.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
transform +
+

Function that takes in a string input and either returns or yields a string output.

+
+

+ + TYPE: + Callable[[str], Generator[str, None, None] | str] + +

+
title +
+

Headline text to display at the top of the UI

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
transform_mode +
+

Specifies how the output should be updated when yielding an output using a generator. + - "append": Concatenates each new piece of text to the existing output. + - "replace": Replaces the existing output with each new piece of text.

+
+

+ + TYPE: + Literal['append', 'replace'] + + + DEFAULT: + 'append' + +

+
+ +
+ +
+ +
+ + +

+ text_io + +

+ + +
+ +

Deprecated: Use text_to_text instead which provides the same functionality +with better default settings.

+

This function creates event handlers for text input and output operations +using the provided transform function to process the input and generate the output.

+ + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
transform +
+

Function that takes in a string input and either returns or yields a string output.

+
+

+ + TYPE: + Callable[[str], Generator[str, None, None] | str] + +

+
title +
+

Headline text to display at the top of the UI

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
transform_mode +
+

Specifies how the output should be updated when yielding an output using a generator. + - "append": Concatenates each new piece of text to the existing output. + - "replace": Replaces the existing output with each new piece of text.

+
+

+ + TYPE: + Literal['append', 'replace'] + + + DEFAULT: + 'replace' + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/text/index.html b/components/text/index.html new file mode 100644 index 000000000..90ec90b16 --- /dev/null +++ b/components/text/index.html @@ -0,0 +1,2542 @@ + + + + + + + + + + + + + + + + + + + + + + + Text - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Text

+ +

Overview

+

Text displays text as-is. If you have markdown text, use the Markdown component.

+

Examples

+ + +
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="/text",
+)
+def text():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text(text="headline-1: Hello, world!", type="headline-1")
+    me.text(text="headline-2: Hello, world!", type="headline-2")
+    me.text(text="headline-3: Hello, world!", type="headline-3")
+    me.text(text="headline-4: Hello, world!", type="headline-4")
+    me.text(text="headline-5: Hello, world!", type="headline-5")
+    me.text(text="headline-6: Hello, world!", type="headline-6")
+    me.text(text="subtitle-1: Hello, world!", type="subtitle-1")
+    me.text(text="subtitle-2: Hello, world!", type="subtitle-2")
+    me.text(text="body-1: Hello, world!", type="body-1")
+    me.text(text="body-2: Hello, world!", type="body-2")
+    me.text(text="caption: Hello, world!", type="caption")
+    me.text(text="button: Hello, world!", type="button")
+
+

API

+ + +
+ + +

+ text + +

+ + +
+ +

Create a text component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
text +
+

The text to display.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
type +
+

The typography level for the text.

+
+

+ + TYPE: + Literal['headline-1', 'headline-2', 'headline-3', 'headline-4', 'headline-5', 'headline-6', 'subtitle-1', 'subtitle-2', 'body-1', 'body-2', 'caption', 'button'] | None + + + DEFAULT: + None + +

+
style +
+

Style to apply to component. Follows HTML Element inline style API.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/textarea/index.html b/components/textarea/index.html new file mode 100644 index 000000000..39517b776 --- /dev/null +++ b/components/textarea/index.html @@ -0,0 +1,3355 @@ + + + + + + + + + + + + + + + + + + + + + + + Textarea - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Textarea

+ +

Overview

+

Textarea allows the user to type in a value and is based on the Angular Material input component for <textarea>.

+

This is similar to Input, but Textarea is better suited for long text inputs.

+

Examples

+ + +
import mesop as me
+
+
+@me.stateclass
+class State:
+  input: str = ""
+  output: str = ""
+
+
+def on_blur(e: me.InputBlurEvent):
+  state = me.state(State)
+  state.input = e.value
+  state.output = e.value
+
+
+def on_newline(e: me.TextareaShortcutEvent):
+  state = me.state(State)
+  state.input = e.value + "\n"
+
+
+def on_submit(e: me.TextareaShortcutEvent):
+  state = me.state(State)
+  state.input = e.value
+  state.output = e.value
+
+
+def on_clear(e: me.TextareaShortcutEvent):
+  state = me.state(State)
+  state.input = ""
+  state.output = ""
+
+
+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="/textarea",
+)
+def app():
+  s = me.state(State)
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.text(
+      "Press enter to submit.",
+      style=me.Style(margin=me.Margin(bottom=15)),
+    )
+    me.text(
+      "Press shift+enter to create new line.",
+      style=me.Style(margin=me.Margin(bottom=15)),
+    )
+    me.text(
+      "Press shift+meta+enter to clear text.",
+      style=me.Style(margin=me.Margin(bottom=15)),
+    )
+    me.textarea(
+      label="Basic input",
+      value=s.input,
+      on_blur=on_blur,
+      shortcuts={
+        me.Shortcut(key="enter"): on_submit,
+        me.Shortcut(shift=True, key="ENTER"): on_newline,
+        me.Shortcut(shift=True, meta=True, key="Enter"): on_clear,
+      },
+      appearance="outline",
+      style=me.Style(width="100%"),
+    )
+    me.text(text=s.output)
+
+

API

+ + +
+ + +

+ textarea + +

+ + +
+ +

Creates a Textarea component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Label for input.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
on_blur +
+

blur is fired when the input has lost focus.

+
+

+ + TYPE: + Callable[[InputBlurEvent], Any] | None + + + DEFAULT: + None + +

+
on_input +
+

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

+
+

+ + TYPE: + Callable[[InputEvent], Any] | None + + + DEFAULT: + None + +

+
autosize +
+

If True, the textarea will automatically adjust its height to fit the content, up to the max_rows limit.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
min_rows +
+

The minimum number of rows the textarea will display.

+
+

+ + TYPE: + int | None + + + DEFAULT: + None + +

+
max_rows +
+

The maximum number of rows the textarea will display.

+
+

+ + TYPE: + int | None + + + DEFAULT: + None + +

+
rows +
+

The number of lines to show in the text area.

+
+

+ + TYPE: + int + + + DEFAULT: + 5 + +

+
appearance +
+

The form field appearance style.

+
+

+ + TYPE: + Literal['fill', 'outline'] + + + DEFAULT: + 'fill' + +

+
style +
+

Style for input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder +
+

Placeholder value

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
required +
+

Whether it's required

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
value +
+

Initial value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
hide_required_marker +
+

Whether the required marker should be hidden.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
color +
+

The color palette for the form field.

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] + + + DEFAULT: + 'primary' + +

+
float_label +
+

Whether the label should always float or float as the user types.

+
+

+ + TYPE: + Literal['always', 'auto'] + + + DEFAULT: + 'auto' + +

+
subscript_sizing +
+

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

+
+

+ + TYPE: + Literal['fixed', 'dynamic'] + + + DEFAULT: + 'fixed' + +

+
hint_label +
+

Text for the form field hint.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
shortcuts +
+

Shortcut events to listen for.

+
+

+ + TYPE: + dict[Shortcut, Callable[[TextareaShortcutEvent], Any]] | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + +

+ native_textarea + +

+ + +
+ +

Creates a browser native Textarea component. Intended for advanced use cases with maximum UI control.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
on_blur +
+

blur is fired when the input has lost focus.

+
+

+ + TYPE: + Callable[[InputBlurEvent], Any] | None + + + DEFAULT: + None + +

+
on_input +
+

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

+
+

+ + TYPE: + Callable[[InputEvent], Any] | None + + + DEFAULT: + None + +

+
autosize +
+

If True, the textarea will automatically adjust its height to fit the content, up to the max_rows limit.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
min_rows +
+

The minimum number of rows the textarea will display.

+
+

+ + TYPE: + int | None + + + DEFAULT: + None + +

+
max_rows +
+

The maximum number of rows the textarea will display.

+
+

+ + TYPE: + int | None + + + DEFAULT: + None + +

+
style +
+

Style for input.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
disabled +
+

Whether it's disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
placeholder +
+

Placeholder value

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
value +
+

Initial value.

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
readonly +
+

Whether the element is readonly.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
shortcuts +
+

Shortcut events to listen for.

+
+

+ + TYPE: + dict[Shortcut, Callable[[TextareaShortcutEvent], Any]] | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ InputBlurEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents an inpur blur event (when a user loses focus of an input).

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ InputEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Represents a user input event.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

Input value.

+
+

+ + TYPE: + str + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/tooltip/index.html b/components/tooltip/index.html new file mode 100644 index 000000000..480e54265 --- /dev/null +++ b/components/tooltip/index.html @@ -0,0 +1,2587 @@ + + + + + + + + + + + + + + + + + + + + + + + Tooltip - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Tooltip

+ +

Overview

+

Tooltip is based on the Angular Material tooltip component.

+

Examples

+ + +
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="/tooltip",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    with me.tooltip(message="Tooltip message"):
+      me.text(text="Hello, World")
+
+

API

+ + +
+ + +

+ tooltip + +

+ + +
+ +

Creates a Tooltip component. +Tooltip is a composite component.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
position +
+

Allows the user to define the position of the tooltip relative to the parent element

+
+

+ + TYPE: + Literal['left', 'right', 'above', 'below', 'before', 'after'] + + + DEFAULT: + 'left' + +

+
position_at_origin +
+

Whether tooltip should be relative to the click or touch origin instead of outside the element bounding box.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Disables the display of the tooltip.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
show_delay_ms +
+

The default delay in ms before showing the tooltip after show is called

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
hide_delay_ms +
+

The default delay in ms before hiding the tooltip after hide is called

+
+

+ + TYPE: + int + + + DEFAULT: + 0 + +

+
message +
+

The message to be displayed in the tooltip

+
+

+ + TYPE: + str + + + DEFAULT: + '' + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/uploader/index.html b/components/uploader/index.html new file mode 100644 index 000000000..3d221d7eb --- /dev/null +++ b/components/uploader/index.html @@ -0,0 +1,2794 @@ + + + + + + + + + + + + + + + + + + + + + + + Uploader - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Uploader

+ +

Overview

+

Uploader is the equivalent of an <input type="file> HTML element except it uses a custom UI that better +matches the look of Angular Material Components.

+

Examples

+

+
import base64
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  file: me.UploadedFile
+
+
+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="/uploader",
+)
+def app():
+  state = me.state(State)
+  with me.box(style=me.Style(padding=me.Padding.all(15))):
+    me.uploader(
+      label="Upload Image",
+      accepted_file_types=["image/jpeg", "image/png"],
+      on_upload=handle_upload,
+      type="flat",
+      color="primary",
+      style=me.Style(font_weight="bold"),
+    )
+
+    if state.file.size:
+      with me.box(style=me.Style(margin=me.Margin.all(10))):
+        me.text(f"File name: {state.file.name}")
+        me.text(f"File size: {state.file.size}")
+        me.text(f"File type: {state.file.mime_type}")
+
+      with me.box(style=me.Style(margin=me.Margin.all(10))):
+        me.image(src=_convert_contents_data_url(state.file))
+
+
+def handle_upload(event: me.UploadEvent):
+  state = me.state(State)
+  state.file = event.file
+
+
+def _convert_contents_data_url(file: me.UploadedFile) -> str:
+  return (
+    f"data:{file.mime_type};base64,{base64.b64encode(file.getvalue()).decode()}"
+  )
+
+

API

+ + +
+ + +

+ uploader + +

+ + +
+ +

This function creates an uploader.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
label +
+

Upload button label.

+
+

+ + TYPE: + str + +

+
accepted_file_types +
+

List of accepted file types. See the accept parameter.

+
+

+ + TYPE: + Sequence[str] | None + + + DEFAULT: + None + +

+
key +
+

The component key.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
on_upload +
+

File upload event handler.

+
+

+ + TYPE: + Callable[[UploadEvent], Any] | None + + + DEFAULT: + None + +

+
type +
+

Type of button style to use

+
+

+ + TYPE: + Literal['raised', 'flat', 'stroked'] | None + + + DEFAULT: + None + +

+
color +
+

Theme color palette of the button

+
+

+ + TYPE: + Literal['primary', 'accent', 'warn'] | None + + + DEFAULT: + None + +

+
disable_ripple +
+

Whether the ripple effect is disabled or not.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
disabled +
+

Whether the button is disabled.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
style +
+

Style for the component.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ UploadEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

Event for file uploads.

+ + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
file +
+

Uploaded file.

+
+

+ + TYPE: + UploadedFile + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + + +

+ UploadedFile + +

+ + +
+

+ Bases: BytesIO

+ + +

Uploaded file contents and metadata.

+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/components/video/index.html b/components/video/index.html new file mode 100644 index 000000000..b68456236 --- /dev/null +++ b/components/video/index.html @@ -0,0 +1,2494 @@ + + + + + + + + + + + + + + + + + + + + + + + Video - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Video

+ +

Overview

+

Video is the equivalent of an <video> HTML element. Video displays the browser's native video controls.

+

Examples

+ + +
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="/video",
+)
+def app():
+  with me.box(style=me.Style(margin=me.Margin.all(15))):
+    me.video(
+      src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm",
+      style=me.Style(height=300, width=300),
+    )
+
+

API

+ + +
+ + +

+ video + +

+ + +
+ +

Creates a video.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
src +
+

URL of the video source

+
+

+ + TYPE: + str + +

+
style +
+

The style to apply to the image, such as width and height.

+
+

+ + TYPE: + Style | None + + + DEFAULT: + None + +

+
+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/demo/index.html b/demo/index.html new file mode 100644 index 000000000..e4f94bfe6 --- /dev/null +++ b/demo/index.html @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + Demo 🌐 - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Demo 🌐

+ + +
+

hide: + - navigation + - toc

+
+ +

+ + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/faq/index.html b/faq/index.html new file mode 100644 index 000000000..099deef15 --- /dev/null +++ b/faq/index.html @@ -0,0 +1,1283 @@ + + + + + + + + + + + + + + + + + + + + + + + FAQ - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Frequently Asked Questions

+

General

+

What kinds of apps is Mesop suited for?

+

Mesop is well-suited for ML/AI demos and internal tools because it enables developers without frontend experience to quickly build web apps. For use cases that prioritize developer experience and velocity, Mesop can be a good choice.

+

Demanding consumer-facing apps, which have strict requirements in terms of performance, custom UI components, and i18n/localization would not be a good fit for Mesop and other UI frameworks may be more suitable.

+

How does Mesop compare to other Python UI frameworks?

+

We have written a comparison doc to answer this question in-depth.

+

Is Mesop production-ready?

+

Dozens of teams at Google have used Mesop to build demos and internal apps.

+

Although Mesop is pre-v1, we take backwards-compatibilty seriously and avoid backwards incompatible change. This is critical to us because many teams within Google rely on Mesop and we need to not break them.

+

Occasionally, we will do minor clean-up for our APIs, but we will provide warnings/deprecation notices and provide at least 1 release to migrate to the newer APIs.

+

Which modules should I import from Mesop?

+

Only import from these two modules:

+
import mesop as me
+import mesop.labs as mel
+
+

All other modules are considered internal implementation details and may change without notice in future releases.

+

Is Mesop an official Google product?

+

No, Mesop is not an official Google product and Mesop is a 20% project maintained by a small core team of Google engineers with contributions from the broader community.

+

Deployment

+

How do I share or deploy my Mesop app?

+

The best way to share your Mesop app is to deploy it to a cloud service. You can follow our deployment guide for step-by-step instructions to deploy to Google Cloud Run.

+
+

Note: you should be able to deploy Mesop on any cloud service that takes a container. Please read the above deployment guide as it should be similar steps.

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/getting-started/core-concepts/index.html b/getting-started/core-concepts/index.html new file mode 100644 index 000000000..6ba932611 --- /dev/null +++ b/getting-started/core-concepts/index.html @@ -0,0 +1,1421 @@ + + + + + + + + + + + + + + + + + + + + + + + Core Concepts - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Core Concepts

+

This doc will explain the core concepts of building a Mesop app.

+

Hello World app

+

Let's start by creating a simple Hello World app in Mesop:

+
import mesop as me
+
+
+@me.page(path="/hello_world")
+def app():
+  me.text("Hello World")
+
+

This simple example demonstrates a few things:

+
    +
  • Every Mesop app starts with import mesop as me. This is the only recommended way to import mesop, otherwise your app may break in the future because you may be relying on internal implementation details.
  • +
  • @me.page is a function decorator which makes a function a root component for a particular path. If you omit the path parameter, this is the equivalent of @me.page(path="/").
  • +
  • app is a Python function that we will call a component because it's creating Mesop components in the body.
  • +
+

Components

+

Components are the building blocks of a Mesop application. A Mesop application is essentially a tree of components.

+

Let's explain the different kinds of components in Mesop:

+
    +
  • Mesop comes built-in with native components. These are components implemented using Angular/Javascript. Many of these components wrap Angular Material components.
  • +
  • You can also create your own components which are called user-defined components. These are essentially Python functions like app in the previous example.
  • +
+

Counter app

+

Let's build a more complex app to demonstrate Mesop's interactivity features.

+
import mesop as me
+
+
+@me.stateclass
+class State:
+  clicks: int
+
+
+def button_click(event: me.ClickEvent):
+  state = me.state(State)
+  state.clicks += 1
+
+
+@me.page(path="/counter")
+def main():
+  state = me.state(State)
+  me.text(f"Clicks: {state.clicks}")
+  me.button("Increment", on_click=button_click)
+
+

This app allows the user to click on a button and increment a counter, which is shown to the user as "Clicks: #".

+

Let's walk through this step-by-step.

+

State

+

The State class represents the application state for a particular browser session. This means every user session has its own instance of State.

+

@me.stateclass is a class decorator which is similar to Python's dataclass but also sets default values based on type hints and allows Mesop to inject the class as shown next.

+
+

Note: Everything in a state class must be serializable because it's sent between the server and browser.

+
+

Event handler

+

The button_click function is an event handler. An event handler has a single parameter, event, which can contain a value (this will be shown in the next example). An event handler is responsible for updating state based on the incoming event.

+

me.state(State) retrieves the instance of the state class for the current session.

+

Component

+

Like the previous example, main is a Mesop component function which is decorated with page to mark it as a root component for a path.

+

Similar to the event handler, we can retrieve the state in a component function by calling me.state(State).

+
+

Note: it's not safe to mutate state inside a component function. All mutations must be done in an event handler.

+
+

Rendering dynamic values in Mesop is simple because you can use standard Python string interpolation use f-strings:

+
me.text(f"Clicks: {state.clicks}")
+
+

The button component demonstrates connecting an event handler to a component. Whenever a click event is triggered by the component, the registered event handler function is called:

+
me.button("Increment", on_click=button_click)
+
+

In summary, you've learned how to define a state class, an event handler and wire them together using interactive components.

+

What's next

+

At this point, you've learned all the basics of building a Mesop app. For a step-by-step guide for building a real-world Mesop application, check out the DuoChat Codelab:

+

+ DuoChat Codelab +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/getting-started/installing/index.html b/getting-started/installing/index.html new file mode 100644 index 000000000..b176076ed --- /dev/null +++ b/getting-started/installing/index.html @@ -0,0 +1,1373 @@ + + + + + + + + + + + + + + + + + + + + + + + Installing - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Installing

+

If you are familiar with setting up a Python environment, then run the following command in your terminal:

+
pip install mesop
+
+

If you're not familiar with setting up a Python environment, follow one of the options below.

+ +

Colab is a free hosted Jupyter notebook product provided by Google.

+

Try Mesop on Colab: Open In Colab

+

B. Command-line

+

If you'd like to run Mesop locally on the command-line, follow these steps.

+

Pre-requisites: Make sure you have Python version 3.10 or later installed by running:

+
python --version
+
+

If you don't, please download Python.

+

Create a venv environment

+
    +
  1. +

    Open the terminal and navigate to a directory: cd foo

    +
  2. +
  3. +

    Create a virtual environment by using venv, which will avoid Python environment issues. Run:

    +
  4. +
+
python -m venv .venv
+
+
    +
  1. +

    Activate your virtual environment:

    +
    +
    +
    +
    source .venv/bin/activate
    +
    +
    +
    +
    .venv\Scripts\activate.bat
    +
    +
    +
    +
    .venv\Scripts\Activate.ps1
    +
    +
    +
    +
    +
  2. +
+

Once you've activated the virtual environment, you will see ".venv" at the start of your terminal prompt.

+
    +
  1. Install mesop:
  2. +
+
pip install mesop
+
+

Upgrading

+

To upgrade Mesop, run the following command:

+
pip install --upgrade mesop
+
+

If you are using requirements.txt or pyproject.toml to manage your dependency versions, then you should update those.

+

Next steps

+

Follow the quickstart guide to learn how to create and run a Mesop app:

+

+ Quickstart +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/getting-started/quickstart/index.html b/getting-started/quickstart/index.html new file mode 100644 index 000000000..08a829b3f --- /dev/null +++ b/getting-started/quickstart/index.html @@ -0,0 +1,1524 @@ + + + + + + + + + + + + + + + + + + + + + + + Quickstart - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Quickstart

+

Let's build a simple interactive Mesop app.

+

Before you start

+

Make sure you've installed Mesop, otherwise please follow the Installing Guide.

+

Starter kit

+

The simplest way to get started with Mesop is to use the starter kit by running mesop init. You can also copy and paste the code.

+
import time
+
+import mesop as me
+
+
+@me.stateclass
+class State:
+  input: str
+  output: str
+  in_progress: bool
+
+
+@me.page(path="/starter_kit")
+def page():
+  with me.box(
+    style=me.Style(
+      background="#fff",
+      min_height="calc(100% - 48px)",
+      padding=me.Padding(bottom=16),
+    )
+  ):
+    with me.box(
+      style=me.Style(
+        width="min(720px, 100%)",
+        margin=me.Margin.symmetric(horizontal="auto"),
+        padding=me.Padding.symmetric(
+          horizontal=16,
+        ),
+      )
+    ):
+      header_text()
+      example_row()
+      chat_input()
+      output()
+  footer()
+
+
+def header_text():
+  with me.box(
+    style=me.Style(
+      padding=me.Padding(
+        top=64,
+        bottom=36,
+      ),
+    )
+  ):
+    me.text(
+      "Mesop Starter Kit",
+      style=me.Style(
+        font_size=36,
+        font_weight=700,
+        background="linear-gradient(90deg, #4285F4, #AA5CDB, #DB4437) text",
+        color="transparent",
+      ),
+    )
+
+
+EXAMPLES = [
+  "How to tie a shoe",
+  "Make a brownie recipe",
+  "Write an email asking for a sick day off",
+]
+
+
+def example_row():
+  is_mobile = me.viewport_size().width < 640
+  with me.box(
+    style=me.Style(
+      display="flex",
+      flex_direction="column" if is_mobile else "row",
+      gap=24,
+      margin=me.Margin(bottom=36),
+    )
+  ):
+    for example in EXAMPLES:
+      example_box(example, is_mobile)
+
+
+def example_box(example: str, is_mobile: bool):
+  with me.box(
+    style=me.Style(
+      width="100%" if is_mobile else 200,
+      height=140,
+      background="#F0F4F9",
+      padding=me.Padding.all(16),
+      font_weight=500,
+      line_height="1.5",
+      border_radius=16,
+      cursor="pointer",
+    ),
+    key=example,
+    on_click=click_example_box,
+  ):
+    me.text(example)
+
+
+def click_example_box(e: me.ClickEvent):
+  state = me.state(State)
+  state.input = e.key
+
+
+def chat_input():
+  state = me.state(State)
+  with me.box(
+    style=me.Style(
+      padding=me.Padding.all(8),
+      background="white",
+      display="flex",
+      width="100%",
+      border=me.Border.all(
+        me.BorderSide(width=0, style="solid", color="black")
+      ),
+      border_radius=12,
+      box_shadow="0 10px 20px #0000000a, 0 2px 6px #0000000a, 0 0 1px #0000000a",
+    )
+  ):
+    with me.box(
+      style=me.Style(
+        flex_grow=1,
+      )
+    ):
+      me.native_textarea(
+        value=state.input,
+        autosize=True,
+        min_rows=4,
+        placeholder="Enter your prompt",
+        style=me.Style(
+          padding=me.Padding(top=16, left=16),
+          background="white",
+          outline="none",
+          width="100%",
+          overflow_y="auto",
+          border=me.Border.all(
+            me.BorderSide(style="none"),
+          ),
+        ),
+        on_blur=textarea_on_blur,
+      )
+    with me.content_button(type="icon", on_click=click_send):
+      me.icon("send")
+
+
+def textarea_on_blur(e: me.InputBlurEvent):
+  state = me.state(State)
+  state.input = e.value
+
+
+def click_send(e: me.ClickEvent):
+  state = me.state(State)
+  if not state.input:
+    return
+  state.in_progress = True
+  input = state.input
+  state.input = ""
+  yield
+
+  for chunk in call_api(input):
+    state.output += chunk
+    yield
+  state.in_progress = False
+  yield
+
+
+def call_api(input):
+  # Replace this with an actual API call
+  time.sleep(0.5)
+  yield "Example of streaming an output"
+  time.sleep(1)
+  yield "\n\nOutput: " + input
+
+
+def output():
+  state = me.state(State)
+  if state.output or state.in_progress:
+    with me.box(
+      style=me.Style(
+        background="#F0F4F9",
+        padding=me.Padding.all(16),
+        border_radius=16,
+        margin=me.Margin(top=36),
+      )
+    ):
+      if state.output:
+        me.markdown(state.output)
+      if state.in_progress:
+        with me.box(style=me.Style(margin=me.Margin(top=16))):
+          me.progress_spinner()
+
+
+def footer():
+  with me.box(
+    style=me.Style(
+      position="sticky",
+      bottom=0,
+      padding=me.Padding.symmetric(vertical=16, horizontal=16),
+      width="100%",
+      background="#F0F4F9",
+      font_size=14,
+    )
+  ):
+    me.html(
+      "Made with <a href='https://google.github.io/mesop/'>Mesop</a>",
+    )
+
+

Running a Mesop app

+

Once you've created your Mesop app using the starter kit, you can run the Mesop app by running the following command in your terminal:

+
mesop main.py
+
+
+

If you've named it something else, replace main.py with the filename of your Python module.

+
+

Open the URL printed in the terminal (i.e. http://localhost:32123) in the browser to see your Mesop app loaded.

+

Hot reload

+

If you make changes to the code, the Mesop app should be automatically hot reloaded. This means that you can keep the mesop CLI command running in the background in your terminal and your UI will automatically be updated in the browser.

+

Next steps

+

Learn more about the core concepts of Mesop as you learn how to build your own Mesop app:

+

+ Core Concepts +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/goals/index.html b/goals/index.html new file mode 100644 index 000000000..100f5fc8a --- /dev/null +++ b/goals/index.html @@ -0,0 +1,1429 @@ + + + + + + + + + + + + + + + + + + + + + + + Goals - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Goals

+

I think it's helpful to explicitly state the goals of a project because it provides clarity for not only the development team, but also developers who are evaluating Mesop amongst other options:

+
    +
  1. Prioritize Python developer experience - Provide the best possible developer experience for Python engineers with minimal frontend experience. Traditional web UI frameworks (e.g. React) prioritize developer experience, but they are focused on web developers who are familiar with the web ecosystem (e.g. HTML, node.js, etc.).
  2. +
  3. Rich user interactions - You should be able to build reasonably sophisticated web applications and demos (e.g. LLM chat) without building custom native components.
  4. +
  5. Simple deployment - Make deployment simple by packaging Mesop into a container which can be deployed as a standalone server.
  6. +
+

Examples of applying these goals

+
    +
  • +

    Web performance: This doesn't mean other goals like web performance have no weight, but we will consistently rank these goals as higher priorities. For example, we could improve performance by serving static assets via CDN, but this would complicate our deployment. For instance, we'd need to ensure that pushing a new Python server binary and JS static assets happened at the same time, or you can get version skews which can cause cryptic errors.

    +
  • +
  • +

    Template vs. code: Mesop adopts the pattern of UI-as-code instead of using a separate templating language. Our belief is that writing Python code is a significantly better learning curve for our target developers. Rather than making them learn a new templating language (DSL) that they are unfamiliar with, they can write Python code which allows them idiomatic ways of expressing conditional logic and looping.

    +
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/auth/index.html b/guides/auth/index.html new file mode 100644 index 000000000..07a377be4 --- /dev/null +++ b/guides/auth/index.html @@ -0,0 +1,1925 @@ + + + + + + + + + + + + + + + + + + + + + + + Auth - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Auth

+

To ensure that the users of your Mesop application are authenticated, this guide provides a detailed, step-by-step process on how to integrate Firebase Authentication with Mesop using a web component.

+

Mesop is designed to be auth provider agnostic, allowing you to integrate any auth library you prefer, whether it's on the client-side (JavaScript) or server-side (Python). You can support sign-ins, including social sign-ins like Google's or any others that you prefer. The general approach involves signing in on the client-side first, then transmitting an auth token to the server-side.

+

Firebase Authentication

+

This guide will walk you through the process of integrating Firebase Authentication with Mesop using a custom web component.

+

Pre-requisites: You will need to create a Firebase account and project. It's free to get started with Firebase and use Firebase auth for small projects, but refer to the pricing page for the most up-to-date information.

+

We will be using three libraries from Firebase to build an end-to-end auth flow:

+
    +
  • Firebase Web SDK: Allows you to call Firebase services from your client-side JavaScript code.
  • +
  • FirebaseUI Web: Provides a simple, customizable auth UI integrated with the Firebase Web SDK.
  • +
  • Firebase Admin SDK (Python): Provides server-side libraries to integrate Firebase services, including Authentication, into your Python applications.
  • +
+

Let's dive into how we will use each one in our Mesop app.

+

Web component

+

The Firebase Authentication web component is a custom component built for handling the user authentication process. It's implemented using Lit, a simple library for building lightweight web components.

+

JS code

+
firebase_auth_component.js
import {
+  LitElement,
+  html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+import 'https://www.gstatic.com/firebasejs/10.0.0/firebase-app-compat.js';
+import 'https://www.gstatic.com/firebasejs/10.0.0/firebase-auth-compat.js';
+import 'https://www.gstatic.com/firebasejs/ui/6.1.0/firebase-ui-auth.js';
+
+// TODO: replace this with your web app's Firebase configuration
+const firebaseConfig = {
+  apiKey: 'AIzaSyAQR9T7sk1lElXTEUBYHx7jv7d_Bs2zt-s',
+  authDomain: 'mesop-auth-test.firebaseapp.com',
+  projectId: 'mesop-auth-test',
+  storageBucket: 'mesop-auth-test.appspot.com',
+  messagingSenderId: '565166920272',
+  appId: '1:565166920272:web:4275481621d8e5ba91b755',
+};
+
+// Initialize Firebase
+firebase.initializeApp(firebaseConfig);
+
+const uiConfig = {
+  // TODO: change this to your Mesop page path.
+  signInSuccessUrl: '/web_component/firebase_auth/firebase_auth_app',
+  signInFlow: 'popup',
+  signInOptions: [firebase.auth.GoogleAuthProvider.PROVIDER_ID],
+  // tosUrl and privacyPolicyUrl accept either url string or a callback
+  // function.
+  // Terms of service url/callback.
+  tosUrl: '<your-tos-url>',
+  // Privacy policy url/callback.
+  privacyPolicyUrl: () => {
+    window.location.assign('<your-privacy-policy-url>');
+  },
+};
+
+// Initialize the FirebaseUI Widget using Firebase.
+const ui = new firebaseui.auth.AuthUI(firebase.auth());
+
+class FirebaseAuthComponent extends LitElement {
+  static properties = {
+    isSignedIn: {type: Boolean},
+    authChanged: {type: String},
+  };
+
+  constructor() {
+    super();
+    this.isSignedIn = false;
+  }
+
+  createRenderRoot() {
+    // Render in light DOM so firebase-ui-auth works.
+    return this;
+  }
+
+  firstUpdated() {
+    firebase.auth().onAuthStateChanged(
+      async (user) => {
+        if (user) {
+          this.isSignedIn = true;
+          const token = await user.getIdToken();
+          this.dispatchEvent(new MesopEvent(this.authChanged, token));
+        } else {
+          this.isSignedIn = false;
+          this.dispatchEvent(new MesopEvent(this.authChanged, ''));
+        }
+      },
+      (error) => {
+        console.log(error);
+      },
+    );
+
+    ui.start('#firebaseui-auth-container', uiConfig);
+  }
+
+  signOut() {
+    firebase.auth().signOut();
+  }
+
+  render() {
+    return html`
+      <div
+        id="firebaseui-auth-container"
+        style="${this.isSignedIn ? 'display: none' : ''}"
+      ></div>
+      <div
+        class="firebaseui-container firebaseui-page-provider-sign-in firebaseui-id-page-provider-sign-in firebaseui-use-spinner"
+        style="${this.isSignedIn ? '' : 'display: none'}"
+      >
+        <button
+          style="background-color:#ffffff"
+          class="firebaseui-idp-button mdl-button mdl-js-button mdl-button--raised firebaseui-idp-google firebaseui-id-idp-button"
+          @click="${this.signOut}"
+        >
+          <span class="firebaseui-idp-text firebaseui-idp-text-long"
+            >Sign out</span
+          >
+        </button>
+      </div>
+    `;
+  }
+}
+
+customElements.define('firebase-auth-component', FirebaseAuthComponent);
+
+

What you need to do:

+
    +
  • Replace firebaseConfig with your Firebase project's config. Read the Firebase docs to learn how to get yours.
  • +
  • Replace the URLs signInSuccessUrl with your Mesop page path and tosUrl and privacyPolicyUrl to your terms and services and privacy policy page respectively.
  • +
+

How it works:

+
    +
  • This creates a simple and configurable auth UI using FirebaseUI Web.
  • +
  • Once the user has signed in, then a sign out button is shown.
  • +
  • Whenever the user signs in or out, the web component dispatches an event to the Mesop server with the auth token, or absence of it.
  • +
  • See our web component docs for more details.
  • +
+

Python code

+
firebase_auth_component.py
from typing import Any, Callable
+
+import mesop.labs as mel
+
+
+@mel.web_component(path="./firebase_auth_component.js")
+def firebase_auth_component(on_auth_changed: Callable[[mel.WebEvent], Any]):
+  return mel.insert_web_component(
+    name="firebase-auth-component",
+    events={
+      "authChanged": on_auth_changed,
+    },
+  )
+
+

How it works:

+
    +
  • Implements the Python side of the Mesop web component. See our web component docs for more details.
  • +
+

Integrating into the app

+

Let's put it all together:

+
firebase_auth_app.py
import firebase_admin
+from firebase_admin import auth
+
+import mesop as me
+import mesop.labs as mel
+from mesop.examples.web_component.firebase_auth.firebase_auth_component import (
+  firebase_auth_component,
+)
+
+# Avoid re-initializing firebase app (useful for avoiding warning message because of hot reloads).
+if firebase_admin._DEFAULT_APP_NAME not in firebase_admin._apps:
+  default_app = firebase_admin.initialize_app()
+
+
+@me.page(
+  path="/web_component/firebase_auth/firebase_auth_app",
+  stylesheets=[
+    "https://www.gstatic.com/firebasejs/ui/6.1.0/firebase-ui-auth.css"
+  ],
+  # Loosen the security policy so the firebase JS libraries work.
+  security_policy=me.SecurityPolicy(
+    dangerously_disable_trusted_types=True,
+    allowed_connect_srcs=["*.googleapis.com"],
+    allowed_script_srcs=[
+      "*.google.com",
+      "https://www.gstatic.com",
+      "https://cdn.jsdelivr.net",
+    ],
+  ),
+)
+def page():
+  email = me.state(State).email
+  if email:
+    me.text("Signed in email: " + email)
+  else:
+    me.text("Not signed in")
+  firebase_auth_component(on_auth_changed=on_auth_changed)
+
+
+@me.stateclass
+class State:
+  email: str
+
+
+def on_auth_changed(e: mel.WebEvent):
+  firebaseAuthToken = e.value
+  if not firebaseAuthToken:
+    me.state(State).email = ""
+    return
+
+  decoded_token = auth.verify_id_token(firebaseAuthToken)
+  # You can do an allowlist if needed.
+  # if decoded_token["email"] != "allowlisted.user@gmail.com":
+  #   raise me.MesopUserException("Invalid user: " + decoded_token["email"])
+  me.state(State).email = decoded_token["email"]
+
+

Note You must add firebase-admin to your Mesop app's requirements.txt file

+

How it works:

+
    +
  • The firebase_auth_app.py module integrates the Firebase Auth web component into the Mesop app. It initializes the Firebase app, defines the page where the Firebase Auth web component will be used, and sets up the state to store the user's email.
  • +
  • The on_auth_changed function is triggered whenever the user's authentication state changes. If the user is signed in, it verifies the user's ID token and stores the user's email in the state. If the user is not signed in, it clears the email from the state.
  • +
+

Next steps

+

Congrats! You've now built an authenticated app with Mesop from start to finish. Read the Firebase Auth docs to learn how to configure additional sign-in options and much more.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/debugging/index.html b/guides/debugging/index.html new file mode 100644 index 000000000..8f66708bd --- /dev/null +++ b/guides/debugging/index.html @@ -0,0 +1,1716 @@ + + + + + + + + + + + + + + + + + + + + + + + Debugging - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Debugging

+

This guide will show you several ways of debugging your Mesop app:

+ +

You can use the first two methods to debug your Mesop app both locally and in production, and the last one to debug your Mesop app locally.

+

Debugging with server logs

+

If your Mesop app is not working properly, we recommend checking the server logs first.

+

If you're running Mesop locally, you can check the terminal. If you're running Mesop in production, you will need to use your cloud provider's console to check the logs.

+

Debugging with Chrome DevTools

+

Chrome DevTools is a powerful set of web developer tools built directly into the Google Chrome browser. It can be incredibly useful for debugging Mesop applications, especially when it comes to inspecting the client-server interactions.

+

Here's how you can use Chrome DevTools to debug your Mesop app:

+
    +
  1. +

    Open your Mesop app in Google Chrome.

    +
  2. +
  3. +

    Right-click anywhere on the page and select "Inspect" or use the keyboard shortcut to open Chrome DevTools:

    +
      +
    • Windows/Linux: Ctrl + Shift + I
    • +
    • macOS: Cmd + Option + I
    • +
    +
  4. +
  5. +

    To debug general errors:

    +
      +
    • Go to the Console tab.
    • +
    • Look for any console error messages.
    • +
    • You can also modify the log levels to display Mesop debug logs by clicking on "Default levels" and selecting "Verbose".
    • +
    +
  6. +
  7. +

    To debug network issues:

    +
      +
    • Go to the Network tab.
    • +
    • Reload your page to see all network requests.
    • +
    • Look for any failed requests (they'll be in red).
    • +
    • Click on a request to see detailed information about headers, response, etc.
    • +
    +
  8. +
  9. +

    For JavaScript errors: + - Check the Console tab for any error messages. + - You can set breakpoints in your JavaScript code using the Sources tab.

    +
  10. +
+

Remember, while Mesop abstracts away much of the frontend complexity, using these tools can still be valuable for debugging and optimizing your app's performance.

+

Debugging with VS Code

+

VS Code is recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs.

+

Pre-requisite: Ensure VS Code is downloaded.

+
    +
  1. +

    Install the Python Debugger VS Code extension.

    +
  2. +
  3. +

    Include the following in your .vscode/launch.json:

    +
  4. +
+
{
+  "version": "0.2.0",
+  "configurations": [
+    {
+      "name": "Python: Remote Attach",
+      "type": "python",
+      "request": "attach",
+      "connect": { "host": "localhost", "port": 5678 },
+      "pathMappings": [
+        { "localRoot": "${workspaceFolder}", "remoteRoot": "." }
+      ],
+      "justMyCode": true
+    }
+  ]
+}
+
+
    +
  1. At the top of your Mesop app (e.g. main.py), including the following snippet to start the debug server:
  2. +
+
import debugpy
+
+debugpy.listen(5678)
+
+
    +
  1. Connect to your debug server by going to the Run & Debug tab in VS Code and selecting "Python: Remote Attach".
  2. +
+

Congrats you are now debugging your Mesop app!

+

To learn more about Python debugging in VS code, read VS Code's Python debugging guide.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/deployment/index.html b/guides/deployment/index.html new file mode 100644 index 000000000..a7081de59 --- /dev/null +++ b/guides/deployment/index.html @@ -0,0 +1,2428 @@ + + + + + + + + + + + + + + + + + + + + + + + Deployment - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Deployment

+
+

We recommend Google Cloud Run or Hugging Face Spaces, which both have a free tier.

+
+

This section describes how to run your Mesop application using the following +platforms:

+ +

If you can run your Mesop app on Docker, you should be able +to run it on many other cloud platforms, such as +Hugging Face Spaces.

+

Example application

+

Let's start with an example application which will consist of the following files:

+
    +
  • main.py
  • +
  • requirements.txt
  • +
+

main.py

+

This file contains your Mesop application code:

+
main.py
import mesop as me
+
+@me.page(title="Home")
+def home():
+  me.text("Hello, world")
+
+

requirements.txt

+

This file specifies the Python dependencies needed. You may need to add additional +dependencies depending on your use case.

+
requirements.txt
mesop
+gunicorn
+
+

Cloud Run

+

We recommend using Google Cloud Run because it's easy to +get started and there's a free tier.

+

Pre-requisites

+

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

+

Procfile

+

Create Procfile to configure gunicorn to run Mesop.

+
Procfile
web: gunicorn --bind :8080 main:me
+
+

The --bind: 8080 will run Mesop on port 8080.

+

The main:me syntax is $(MODULE_NAME):$(VARIABLE_NAME): (see Gunicorn docs for more details):

+
    +
  • Because the Mesop python file is main.py, the module name is main.
  • +
  • By convention, we do import mesop as me so the me refers to the main Mesop + library module which is also a callable (e.g. a function) that conforms to WSGI.
  • +
+

Deploy to Google Cloud Run

+

In your terminal, go to the application directory, which has the files listed above.

+

Run the following command:

+
gcloud run deploy
+
+

Follow the instructions and then you should be able to access your deployed app.

+

Session Affinity

+

If you're running Mesop with MESOP_STATE_SESSION_BACKEND=memory, +then you will want to enable session affinity in order to utilize the memory backend efficiently.

+

The command should be:

+
gcloud run services update $YOUR_SERVICE --session-affinity
+
+

By default gunicorn allocates one worker, but you should double check that gunicorn is +configured correctly for the memory backend.

+

App Engine

+

This section describes deployment to Google App Engine using +their flexible environments feature.

+

Pre-requisites

+

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

+

You will also need to run:

+
gcloud app create --project=[YOUR_PROJECT_ID]
+gcloud components install app-engine-python
+
+

app.yaml

+

Create app.yaml to configure App Engine to run Mesop.

+
app.yaml
runtime: python
+env: flex
+entrypoint: gunicorn -b :$PORT main:me
+
+runtime_config:
+  operating_system: ubuntu22
+  runtime_version: "3.10"
+
+manual_scaling:
+  instances: 1
+
+resources:
+  cpu: 1
+  memory_gb: 0.5
+  disk_size_gb: 10
+
+

Deploy to App Engine

+

In your terminal, go to the application directory, which has the files listed above.

+

Run the following command:

+
gcloud app deploy
+
+

Follow the instructions and then you should be able to access your deployed app.

+

Docker

+

If you can run your Mesop app on Docker, you should be able +to run it on many other cloud platforms.

+

Pre-requisites

+

Make sure Docker and Docker Compose are installed.

+

Dockerfile

+
Dockerfile
FROM python:3.10.15-bullseye
+
+RUN apt-get update && \
+  apt-get install -y \
+  # General dependencies
+  locales \
+  locales-all && \
+  # Clean local repository of package files since they won't be needed anymore.
+  # Make sure this line is called after all apt-get update/install commands have
+  # run.
+  apt-get clean && \
+  # Also delete the index files which we also don't need anymore.
+  rm -rf /var/lib/apt/lists/*
+
+ENV LC_ALL en_US.UTF-8
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US.UTF-8
+
+# Install dependencies
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+# Create non-root user
+RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop
+USER mesop
+
+# Add app code here
+COPY . /srv/mesop-app
+WORKDIR /srv/mesop-app
+
+# Run Mesop through gunicorn. Should be available at localhost:8080
+CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:me"]
+
+

docker-compose.yaml

+
docker-compose.yaml
services:
+  mesop-app:
+    build: .
+    ports:
+      - "8080:8080"
+
+

Run Docker image

+

In your terminal, go to the application directory, which has the files listed above.

+

Run the following command:

+
docker-compose up -d
+
+

Alternatively, if you do not want to use Docker Compose, you can run:

+
docker build -t mesop-app . && docker run -d -p 8080:8080 mesop-app
+
+

You should now be able to view your Mesop app at http://localhost:8080.

+

Hugging Face Spaces

+

Hugging Face Spaces has a free tier that gives you 2 vCPU and 16GB RAM, which is plenty +for running Mesop applications that leverage generative AI APIs.

+

Pre-requisites

+

This section assumes you already have a free Hugging Face Space account.

+

Create new Space

+

Go to https://huggingface.co/spaces and click +Create new Space.

+

Create new Hugging Face Space

+

Name your app and use Docker SDK

+

Name the Space mesop-hello-world you want and select the apache-2.0 license.

+

Next select the Docker SDK with a blank template.

+

Select Docker

+

CPU Basic and Create Space

+

Next make sure that you are using the free CPU Basic plan. Then click Create Space.

+

Create Hugging Face Space

+

Clone your Hugging Face Space Git Repository

+

Example command using Git over SSH:

+
git clone git@hf.co:spaces/<user-name>/mesop-hello-world
+cd mesop-hello-world
+
+
+

Note: You'll need to have an SSH key configured on Hugging Face. See https://huggingface.co/docs/hub/en/security-git-ssh.

+
+

Create main.py

+

This is the same main.py file shown earlier, except we need to allow Hugging Face to +iframe our Mesop app.

+
main.py
import mesop as me
+
+@me.page(
+  title="Home",
+  security_policy=me.SecurityPolicy(
+    allowed_iframe_parents=["https://huggingface.co"]
+  ),
+)
+def home():
+  me.text("Hello, world")
+
+

Create requirements.txt

+

This file is the same as the generic Docker setup:

+
requirements.txt
mesop
+gunicorn
+
+

Create Dockerfile

+

This file is the same as the generic Docker setup:

+
Dockerfile
FROM python:3.10.15-bullseye
+
+RUN apt-get update && \
+  apt-get install -y \
+  # General dependencies
+  locales \
+  locales-all && \
+  # Clean local repository of package files since they won't be needed anymore.
+  # Make sure this line is called after all apt-get update/install commands have
+  # run.
+  apt-get clean && \
+  # Also delete the index files which we also don't need anymore.
+  rm -rf /var/lib/apt/lists/*
+
+ENV LC_ALL en_US.UTF-8
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US.UTF-8
+
+# Install dependencies
+COPY requirements.txt .
+RUN pip install -r requirements.txt
+
+# Create non-root user
+RUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop
+USER mesop
+
+# Add app code here
+COPY . /srv/mesop-app
+WORKDIR /srv/mesop-app
+
+# Run Mesop through gunicorn. Should be available at localhost:8080
+CMD ["gunicorn", "--bind", "0.0.0.0:8080", "main:me"]
+
+

Add app_port in README.md

+

Next we will need to open port 8080 which we specified in the Dockerfile. This is +done through a config section in the README.md.

+
README.md
---
+title: Mesop Hello World
+emoji: 🐠
+colorFrom: blue
+colorTo: purple
+sdk: docker
+pinned: false
+license: apache-2.0
+app_port: 8080
+---
+
+Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
+
+

Deploy to Hugging Face Spaces

+

The commands to commit your changes and push to the Hugging Face Spaces git repository +are:

+
git add -A
+git commit -m "Add hello world Mesop app"
+git push origin main
+
+

View deployed app

+

Congratulations! You should now be able to view your app on Hugging Face Spaces.

+

The URL should be something like this:

+
https://huggingface.co/spaces/<user-name>/mesop-hello-world
+
+

Your deployed Hugging Face Spaces app

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/event-handlers/index.html b/guides/event-handlers/index.html new file mode 100644 index 000000000..ba0259c62 --- /dev/null +++ b/guides/event-handlers/index.html @@ -0,0 +1,1939 @@ + + + + + + + + + + + + + + + + + + + + + + + Event Handlers - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Event Handlers

+

Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions which are called by the Mesop framework when a user event is received.

+

How it works

+

Let's take a look at a simple example of an event handler:

+
Simple event handler
def counter():
+    me.button("Increment", on_click=on_click)
+
+def on_click(event: me.ClickEvent):
+    state = me.state(State)
+    state.count += 1
+
+@me.stateclass
+class State:
+    count: int = 0
+
+

Although this example looks simple, there's a lot going on under the hood.

+

When the counter function is called, it creates an instance of the button component and binds the on_click event handler to it. Because components (and the entire Mesop UI) is serialized and sent to the client, we need a way of serializing the event handler so that when the button is clicked, the correct event handler is called on the server.

+

We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function.

+

Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault-tolerant and enables simple scaling.

+

Types of event handlers

+

Regular functions

+

These are the simplest and most common type of event handlers used. It's essentially a regular Python function which is called by the Mesop framework when a user event is received.

+
Regular function
def on_click(event: me.ClickEvent):
+    state = me.state(State)
+    state.count += 1
+
+

Generator functions

+

Python Generator functions are a powerful tool, which allow you to yield multiple times in a single event handler. This allows you to render intermediate UI states.

+
Generator function
def on_click(event: me.ClickEvent):
+    state = me.state(State)
+    state.count += 1
+    yield
+    time.sleep(1)
+    state.count += 1
+    yield
+
+

You can learn more about real-world use cases of the generator functions in the Interactivity guide.

+
+Always yield at the end of a generator function +

If you use a yield statement in your event handler, then the event handler will be a generator function. You must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

+
+

Async generator functions

+

Python async generator functions allow you to do concurrent work using Python's async and await language features. If you are using async Python libraries, you can use these types of event handlers.

+
Async generator function
async def on_click(event: me.ClickEvent):
+    state = me.state(State)
+    state.count += 1
+    yield
+    await asyncio.sleep(1)
+    state.count += 1
+    yield
+
+

For a more complete example, please refer to the Async section of the Interactivity guide.

+
+Always yield at the end of an async generator function +

Similar to a regular generator function, an async generator function must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

+
+

Patterns

+

Reusing event handler logic

+

You can share event handler logic by extracting the common logic into a separate function. For example, you will often want to use the same logic for the on_enter event handler for an input component and the on_click event handler for a "send" button component.

+
Reusing event handler logic
def on_enter(event: me.InputEnterEvent):
+    state = me.state(State)
+    state.value = event.value
+    call_api()
+
+def on_click(event: me.ClickEvent):
+    # Assumes that state.value has been set by an on_blur event handler
+    call_api()
+
+def call_api():
+    # Put your common event handler logic here
+    pass
+
+

If you want to reuse event handler logic between generator functions, you can use the yield from syntax. For example, let's say call_api in the above example is a generator function. You can use yield from to reuse the event handler logic:

+
Reusing event handler logic for generator functions
def on_enter(event: me.InputEnterEvent):
+    state = me.state(State)
+    state.value = event.value
+    yield from call_api()
+
+def on_click(event: me.ClickEvent):
+    yield from call_api()
+
+def call_api():
+    # Do initial work
+    yield
+    # Do more work
+    yield
+
+

Boilerplate-free event handlers

+

If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the key attribute that's available in most events and uses Python's built-in setattr function to dynamically update the state:

+
Boilerplate-free event handlers
def app():
+  me.input(label="Name", key="name", on_blur=update_state)
+  me.input(label="Address", key="address", on_blur=update_state)
+
+@me.stateclass
+class State:
+  name: str
+  address: str
+
+def update_state(event: me.InputBlurEvent):
+  state = me.state(State)
+  setattr(state, event.key, event.value)
+
+

The downside of this approach is that you lose type safety. Generally, defining a separate event handler, although more verbose, is easier to maintain.

+

Troubleshooting

+

Avoid using closure variables in event handler

+

One subtle mistake when building a reusable component is having the event handler use a closure variable, as shown in the following example:

+
Bad example of using closure variable
@me.component
+def link_component(url: str):
+   def on_click(event: me.ClickEvent):
+     me.navigate(url)
+  return me.button(url, on_click=on_click)
+
+def app():
+    link_component("/1")
+    link_component("/2")
+
+

The problem with this above example is that Mesop only stores the last event handler. This is because each event handler has the same id which means that Mesop cannot differentiate between the two instances of the same event handler.

+

This means that both instances of the link_component will refer to the last on_click instance which references the same url closure variable set to "/2". This almost always produces the wrong behavior.

+

Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example:

+
Good example of using key
@me.component
+def link_component(url: str):
+   def on_click(event: me.ClickEvent):
+     me.navigate(event.key)
+  return me.button(url, key=url, on_click=on_click)
+
+

For more info on using component keys, please refer to the Component Key docs.

+

Next steps

+

Explore advanced interactivity patterns like streaming and async:

+

+ Interactivity +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/interactivity/index.html b/guides/interactivity/index.html new file mode 100644 index 000000000..24b1656d6 --- /dev/null +++ b/guides/interactivity/index.html @@ -0,0 +1,1956 @@ + + + + + + + + + + + + + + + + + + + + + + + Interactivity - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Interactivity

+

This guide continues from the event handlers guide and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call.

+

Intermediate loading state

+

If you are calling a slow blocking API (e.g. several seconds) to provide a better user experience, you may want to introduce a custom loading indicator for a specific event.

+
+

Note: Mesop has a built-in loading indicator at the top of the page for all events.

+
+
import time
+
+import mesop as me
+
+
+def slow_blocking_api_call():
+  time.sleep(2)
+  return "foo"
+
+
+@me.stateclass
+class State:
+  data: str
+  is_loading: bool
+
+
+def button_click(event: me.ClickEvent):
+  state = me.state(State)
+  state.is_loading = True
+  yield
+  data = slow_blocking_api_call()
+  state.data = data
+  state.is_loading = False
+  yield
+
+
+@me.page(path="/loading")
+def main():
+  state = me.state(State)
+  if state.is_loading:
+    me.progress_spinner()
+  me.text(state.data)
+  me.button("Call API", on_click=button_click)
+
+

In this example, our event handler is a Python generator function. Each yield statement yields control back to the Mesop framework and executes a render loop which results in a UI update.

+

Before the first yield statement, we set is_loading to True on state so we can show a spinner while the user is waiting for the slow API call to complete.

+

Before the second (and final) yield statement, we set is_loading to False, so we can hide the spinner and then we add the result of the API call to state so we can display that to the user.

+
+

Tip: you must have a yield statement as the last line of a generator event handler function. Otherwise, any code after the final yield will not be executed.

+
+

Streaming

+

This example builds off the previous Loading example and makes our event handler a generator function so we can incrementally update the UI.

+
from time import sleep
+
+import mesop as me
+
+
+def generate_str():
+  yield "foo"
+  sleep(1)
+  yield "bar"
+
+
+@me.stateclass
+class State:
+  string: str = ""
+
+
+def button_click(action: me.ClickEvent):
+  state = me.state(State)
+  for val in generate_str():
+    state.string += val
+    yield
+
+
+@me.page(path="/streaming")
+def main():
+  state = me.state(State)
+  me.button("click", on_click=button_click)
+  me.text(text=f"{state.string}")
+
+

Async

+

If you want to do multiple long-running operations concurrently, then we recommend you to use Python's async and await.

+
import asyncio
+
+import mesop as me
+
+
+@me.page(path="/async_await")
+def page():
+  s = me.state(State)
+  me.text("val1=" + s.val1)
+  me.text("val2=" + s.val2)
+  me.button("async with yield", on_click=click_async_with_yield)
+  me.button("async without yield", on_click=click_async_no_yield)
+
+
+@me.stateclass
+class State:
+  val1: str
+  val2: str
+
+
+async def fetch_dummy_values():
+  # Simulate an asynchronous operation
+  await asyncio.sleep(2)
+  return "<async_value>"
+
+
+async def click_async_with_yield(e: me.ClickEvent):
+  val1_task = asyncio.create_task(fetch_dummy_values())
+  val2_task = asyncio.create_task(fetch_dummy_values())
+
+  me.state(State).val1, me.state(State).val2 = await asyncio.gather(
+    val1_task, val2_task
+  )
+  yield
+
+
+async def click_async_no_yield(e: me.ClickEvent):
+  val1_task = asyncio.create_task(fetch_dummy_values())
+  val2_task = asyncio.create_task(fetch_dummy_values())
+
+  me.state(State).val1, me.state(State).val2 = await asyncio.gather(
+    val1_task, val2_task
+  )
+
+

Troubleshooting

+

User input race condition

+

If you notice a race condition with user input (e.g. input or textarea) where sometimes the last few characters typed by the user is lost, you are probably unnecessarily setting the value of the component.

+

See the following example using this anti-pattern ⚠:

+
Bad example: setting the value and using on_input
@me.stateclass
+class State:
+  input_value: str
+
+def app():
+  state = me.state(State)
+  me.input(value=state.input_value, on_input=on_input)
+
+def on_input(event: me.InputEvent):
+  state = me.state(State)
+  state.input_value = event.value
+
+

The problem is that the input value now has a race condition because it's being set by two sources:

+
    +
  1. The server is setting the input value based on state.
  2. +
  3. The client is setting the input value based on what the user is typing.
  4. +
+

There's several ways to fix this which are shown below.

+

Option 1: Use on_blur instead of on_input

+

You can use the on_blur event instead of on_input to only update the input value when the user loses focus on the input field.

+

This is also more performant because it sends much fewer network requests.

+
Good example: setting the value and using on_input
@me.stateclass
+class State:
+  input_value: str
+
+def app():
+  state = me.state(State)
+  me.input(value=state.input_value, on_input=on_input)
+
+def on_input(event: me.InputEvent):
+  state = me.state(State)
+  state.input_value = event.value
+
+

Option 2: Do not set the input value from the server

+

If you don't need to set the input value from the server, then you can remove the value attribute from the input component.

+
Good example: not setting the value
@me.stateclass
+class State:
+  input_value: str
+
+def app():
+  state = me.state(State)
+  me.input(on_input=on_input)
+
+def on_input(event: me.InputEvent):
+  state = me.state(State)
+  state.input_value = event.value
+
+

Option 3: Use two separate variables for initial and current input value

+

If you need set the input value from the server and you need to use on_input, then you can use two separate variables for the initial and current input value.

+
Good example: using two separate variables for initial and current input value
@me.stateclass
+class State:
+  initial_input_value: str = "initial_value"
+  current_input_value: str
+
+@me.page()
+def app():
+  state = me.state(State)
+  me.input(value=state.initial_input_value, on_input=on_input)
+
+def on_input(event: me.InputEvent):
+  state = me.state(State)
+  state.current_input_value = event.value
+
+

Next steps

+

Learn about layouts to build a customized UI.

+

+ Layouts +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/labs/index.html b/guides/labs/index.html new file mode 100644 index 000000000..341826aaf --- /dev/null +++ b/guides/labs/index.html @@ -0,0 +1,1599 @@ + + + + + + + + + + + + + + + + + + + + + + + Labs - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Labs

+

Mesop Labs is built on top of the core Mesop framework and may evolve in the future.

+

Using Labs

+

You will need to add an import statement to use labs:

+
import mesop.labs as mel
+
+

The code inside Mesop Labs is intended to be simple to understand so you can copy and customize it as needed.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/layouts/index.html b/guides/layouts/index.html new file mode 100644 index 000000000..783e07461 --- /dev/null +++ b/guides/layouts/index.html @@ -0,0 +1,1974 @@ + + + + + + + + + + + + + + + + + + + + + + + Layouts - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Layouts

+

Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app so you can build custom layouts. The crux of doing layouts in Mesop is the Box component and using the Style API which are Pythonic wrappers around the CSS layout model.

+

For most Mesop apps, you will use some combination of these types of layouts:

+ +

Common layout examples

+

To interact with the examples below, open the Mesop Layouts Colab: Open In Colab

+

Rows and Columns

+

Basic Row

+
Basic Row
def row():
+    with me.box(style=me.Style(display="flex", flex_direction="row")):
+        me.text("Left")
+        me.text("Right")
+
+

Row with Spacing

+
Row with Spacing
def row():
+    with me.box(style=me.Style(display="flex", flex_direction="row", justify_content="space-around")):
+        me.text("Left")
+        me.text("Right")
+
+

Row with Alignment

+
Row with Alignment
def row():
+    with me.box(style=me.Style(display="flex", flex_direction="row", align_items="center")):
+        me.box(style=me.Style(background="red", height=50, width="50%"))
+        me.box(style=me.Style(background="blue", height=100, width="50%"))
+
+

Rows and Columns

+
Rows and Columns
def app():
+    with me.box(style=me.Style(display="flex", flex_direction="row", gap=16, height="100%")):
+        column(1)
+        column(2)
+        column(3)
+
+def column(num: int):
+    with me.box(style=me.Style(
+        flex_grow=1,
+        background="#e0e0e0",
+        padding=me.Padding.all(16),
+        display="flex",
+        flex_direction="column",
+    )):
+        me.box(style=me.Style(background="red", height=100))
+        me.box(style=me.Style(background="blue", flex_grow=1))
+
+

Grids

+

Side-by-side Grid

+
Side-by-side Grid
def grid():
+    # 1fr means 1 fraction, so each side is the same size.
+    # Try changing one of the 1fr to 2fr and see what it looks like
+    with me.box(style=me.Style(display="grid", grid_template_columns="1fr 1fr")):
+        me.text("A bunch of text")
+        me.text("Some more text")
+
+ +
Header Body Footer Grid
def app():
+    with me.box(style=me.Style(
+        display="grid",
+        grid_template_rows="auto 1fr auto",
+        height="100%"
+    )):
+        # Header
+        with me.box(style=me.Style(
+            background="#f0f0f0",
+            padding=me.Padding.all(24)
+        )):
+            me.text("Header")
+
+        # Body
+        with me.box(style=me.Style(
+            padding=me.Padding.all(24),
+            overflow_y="auto"
+        )):
+            me.text("Body Content")
+            # Add more body content here
+
+        # Footer
+        with me.box(style=me.Style(
+            background="#f0f0f0",
+            padding=me.Padding.all(24)
+        )):
+            me.text("Footer")
+
+ +
Sidebar Layout
def app():
+    with me.box(style=me.Style(
+        display="grid",
+        grid_template_columns="250px 1fr",
+        height="100%"
+    )):
+        # Sidebar
+        with me.box(style=me.Style(
+            background="#f0f0f0",
+            padding=me.Padding.all(24),
+            overflow_y="auto"
+        )):
+            me.text("Sidebar")
+
+        # Main content
+        with me.box(style=me.Style(
+            padding=me.Padding.all(24),
+            overflow_y="auto"
+        )):
+            me.text("Main Content")
+
+

Responsive UI

+

This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes.

+

Learn more about responsive UI in the viewport size docs.

+
def app():
+    is_desktop = me.viewport_size().width > 640
+    with me.box(style=me.Style(
+        display="grid",
+        grid_template_columns="250px 1fr" if is_desktop else "1fr",
+        height="100%"
+    )):
+        if is_desktop:
+          # Sidebar
+          with me.box(style=me.Style(
+              background="#f0f0f0",
+              padding=me.Padding.all(24),
+              overflow_y="auto"
+          )):
+              me.text("Sidebar")
+
+        # Main content
+        with me.box(style=me.Style(
+            padding=me.Padding.all(24),
+            overflow_y="auto"
+        )):
+            me.text("Main Content")
+
+

Learn more

+

For a real-world example of using these types of layouts, check out the Mesop Showcase app:

+ +

To learn more about flexbox layouts (rows and columns), check out:

+ +

To learn more about grid layouts, check out:

+ + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/multi-pages/index.html b/guides/multi-pages/index.html new file mode 100644 index 000000000..a055e04cb --- /dev/null +++ b/guides/multi-pages/index.html @@ -0,0 +1,1654 @@ + + + + + + + + + + + + + + + + + + + + + + + Multi-Pages - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Multi-Pages

+

You can define multi-page Mesop applications by using the page feature you learned from Core Concepts.

+

Multi-page setup

+
import mesop as me
+
+@me.page(path="/1")
+def page1():
+    me.text("page 1")
+
+@me.page(path="/2")
+def page2():
+    me.text("page 2")
+
+

Learn more about page configuration in the page API doc.

+ +

If you have multiple pages, you will typically want to navigate from one page to another when the user clicks a button. You can use me.navigate("/to/path") to navigate to another page.

+

Example:

+
import mesop as me
+
+
+def on_click(e: me.ClickEvent):
+  state = me.state(State)
+  state.count += 1
+  me.navigate("/multi_page_nav/page_2")
+
+
+@me.page(path="/multi_page_nav")
+def main_page():
+  me.button("Navigate to Page 2", on_click=on_click)
+
+
+@me.page(path="/multi_page_nav/page_2")
+def page_2():
+  state = me.state(State)
+  me.text(f"Page 2 - count: {state.count}")
+
+
+@me.stateclass
+class State:
+  count: int
+
+
+

Note: you can re-use state across pages. See how the above example uses the State#count value across pages.

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/performance/index.html b/guides/performance/index.html new file mode 100644 index 000000000..4684b15e6 --- /dev/null +++ b/guides/performance/index.html @@ -0,0 +1,1770 @@ + + + + + + + + + + + + + + + + + + + + + + + Performance - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Performance

+

Occasionally, you may run into performance issues with your Mesop app. Here are some tips to help you improve your app's performance.

+

Determine the root cause

+

The first step in debugging performance issues is to identify the cause of the issue. Follow the Debugging with DevTools guide and use the Console and Network tab to identify the issue.

+

Common performance bottlenecks and solutions

+

Optimizing state size

+

If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large.

+

Because the state object is serialized and sent back and forth between the client and server, you should try to keep the state object reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app.

+

The following are recommendations to help you avoid large state payloads:

+

Store state in memory

+

Mesop has a feature that allows you to store state in memory rather than passing the +full state on every request. This may help improve performance when dealing with large +state objects. The caveat is that, storing state in memory contains its own set of +problems that you must carefully consider. See the config section +for details on how to use this feature.

+

If you are running Mesop on a single replica or you can enable session affinity, then this is a good option.

+

Store state externally

+

You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store them in a bucket service like Google Cloud Storage and send signed URLs to the client so that it can directly fetch the images without going through the Mesop server.

+

Handling high user load

+

If you notice that your Mesop app is running slowly when you have many concurrent users, you can try to scale your Mesop app.

+

Increase the number of replicas

+

To handle more concurrent users, you can scale your Mesop app horizontally by increasing the number of replicas (instances) running your application. This can be achieved through various cloud services that offer autoscaling features:

+
    +
  1. +

    Use a managed service like Google Cloud Run, which automatically scales your app based on traffic. Follow Mesop's Cloud Run deployment guide for details.

    +
  2. +
  3. +

    Manually adjust the number of replicas to a higher number.

    +
  4. +
  5. +

    Tune gunicorn settings. If you're using gunicorn to serve your Mesop app, you can adjust gunicorn settings to increase the number of workers. This can help to increase the number of concurrent users your Mesop app can handle.

    +
  6. +
+

Whichever platform you choose, make sure to configure the replica settings to match your app's performance requirements and budget constraints.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/server-integration/index.html b/guides/server-integration/index.html new file mode 100644 index 000000000..15232d436 --- /dev/null +++ b/guides/server-integration/index.html @@ -0,0 +1,1702 @@ + + + + + + + + + + + + + + + + + + + + + + + Server integration - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Server integration

+

Mesop allows you to integrate Mesop with other Python web servers like FastAPI or Flask by mounting the Mesop app which is a WSGI app.

+

This enables you to do things like:

+
    +
  • Serve local files (e.g. images)
  • +
  • Provide API endpoints (which can be called by the web component, etc.)
  • +
+

API

+

The main API for doing this integration is the create_wsgi_app function.

+ + +
+ + +

+ create_wsgi_app + +

+ + +
+ +

Creates a WSGI app that can be used to run Mesop in a WSGI server like gunicorn.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
debug_mode +
+

If True, enables debug mode for the Mesop app.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
+ +
+ +

FastAPI example

+

For a working example of using Mesop with FastAPI, please take a look at this repo: +https://github.com/wwwillchen/mesop-fastapi

+
+

Note: you can apply similar steps to use any other web framework that allows you to mount a WSGI app.

+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/state-management/index.html b/guides/state-management/index.html new file mode 100644 index 000000000..e79172143 --- /dev/null +++ b/guides/state-management/index.html @@ -0,0 +1,1897 @@ + + + + + + + + + + + + + + + + + + + + + + + State Management - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

State Management

+

State management is a critical element of building interactive apps because it allows you store information about what the user did in a structured way.

+

Basic usage

+

You can register a class using the class decorator me.stateclass which is like a dataclass with special powers:

+
@me.stateclass
+class State:
+  val: str
+
+

You can get an instance of the state class inside any of your Mesop component functions by using me.state:

+
@me.page()
+def page():
+    state = me.state(State)
+    me.text(state.val)
+
+

Use immutable default values

+

Similar to regular dataclasses which disallow mutable default values, you need to avoid mutable default values such as list and dict for state classes. Using mutable default values can result in leaking state across sessions which can be a serious privacy issue.

+

You MUST use immutable default values or use dataclasses field initializer or not set a default value.

+
+Good: immutable default value +

Setting a default value to an immutable type like str is OK.

+
@me.stateclass
+class State:
+  a: str = "abc"
+
+
+
+Bad: mutable default value +

The following will raise an exception because dataclasses prevents you from using mutable collection types like list as the default value because this is a common footgun.

+
@me.stateclass
+class State:
+  a: list[str] = ["abc"]
+
+

If you set a default value to an instance of a custom type, an exception will not be raised, but you will be dangerously sharing the same mutable instance across sessions which could cause a serious privacy issue.

+
@me.stateclass
+class State:
+  a: MutableClass = MutableClass()
+
+
+
+Good: default factory +

If you want to set a field to a mutable default value, use default_factory in the field function from the dataclasses module to create a new instance of the mutable default value for each instance of the state class.

+
from dataclasses import field
+
+@me.stateclass
+class State:
+  a: list[str] = field(default_factory=lambda: ["abc"])
+
+
+
+Good: no default value +

If you want a default of an empty list, you can just not define a default value and Mesop will automatically define an empty list default value.

+

For example, if you write the following:

+
@me.stateclass
+class State:
+  a: list[str]
+
+

It's the equivalent of:

+
@me.stateclass
+class State:
+  a: list[str] = field(default_factory=list)
+
+
+

How State Works

+

me.stateclass is a class decorator which tells Mesop that this class can be retrieved using the me.state method, which will return the state instance for the current user session.

+
+

If you are familiar with the dependency injection pattern, Mesop's stateclass and state API is essentially a minimalist dependency injection system which scopes the state object to the lifetime of a user session.

+
+

Under the hood, Mesop is sending the state back and forth between the server and browser client so everything in a state class must be serializable.

+

Multiple state classes

+

You can use multiple classes to store state for the current user session.

+

Using different state classes for different pages or components can help make your app easier to maintain and more modular.

+
@me.stateclass
+class PageAState:
+    ...
+
+@me.stateclass
+class PageBState:
+    ...
+
+@me.page(path="/a")
+def page_a():
+    state = me.state(PageAState)
+    ...
+
+@me.page(path="/b")
+def page_b():
+    state = me.state(PageBState)
+    ...
+
+

Under the hood, Mesop is managing state classes based on the identity (e.g. module name and class name) of the state class, which means that you could have two state classes named "State", but if they are in different modules, then they will be treated as separate state, which is what you would expect.

+

Nested State

+

You can also have classes inside of a state class as long as everything is serializable:

+
class NestedState:
+  val: str
+
+@me.stateclass
+class State:
+  nested: NestedState
+
+def app():
+  state = me.state(State)
+
+
+

Note: you only need to decorate the top-level state class with @me.stateclass. All the nested state classes will automatically be wrapped.

+
+

Nested State and dataclass

+

Sometimes, you may want to explicitly decorate the nested state class with dataclass because in the previous example, you couldn't directly instantiate NestedState.

+

If you wanted to use NestedState as a general dataclass, you can do the following:

+
@dataclass
+class NestedState:
+  val: str = ""
+
+@me.stateclass
+class State:
+  nested: NestedState
+
+def app():
+  state = me.state(State)
+
+
+

Reminder: because dataclasses do not have default values, you will need to explicitly set default values, otherwise Mesop will not be able to instantiate an empty version of the class.

+
+

Now, if you have an event handler function, you can do the following:

+
def on_click(e):
+    response = call_api()
+    state = me.state(State)
+    state.nested = NestedState(val=response.text)
+
+

If you didn't explicitly annotate NestedState as a dataclass, then you would get an error instantiating NestedState because there's no initializer defined.

+

Tips

+

State performance issues

+

Take a look at the performance guide to learn how to identify and fix State-related performance issues.

+

Next steps

+

Event handlers complement state management by providing a way to update your state in response to user interactions.

+

+ Event handlers +

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/static-assets/index.html b/guides/static-assets/index.html new file mode 100644 index 000000000..206b0e858 --- /dev/null +++ b/guides/static-assets/index.html @@ -0,0 +1,1822 @@ + + + + + + + + + + + + + + + + + + + + + + + Static assets - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Static Assets

+

Mesop allows you to specify a folder for storing static assets that will be served by +the Mesop server.

+

This feature provides a simple way to serving images, CSS stylesheets, and other files +without having to rely on CDNs, external servers, or mounting Mesop onto FastAPI/Flask.

+

Enable a static folder

+

This feature can be enabled using environment variables.

+ +

Full descriptions of these two settings can be found on the config page.

+

Enabling a static folder named "assets"

+

This will make the files in the assets directory accessible from the Mesop server +at /static.

+

Mesop will look for the assets directory relative to your current working directory. +In this case, /some/path/mesop-app/assets.

+
cd /some/path/mesop-app
+MESOP_STATIC_FOLDER=assets mesop main.py
+
+

Here is another example:

+

Mesop will look for the assets directory relative to your current working directory. +In this case, /some/path/assets.

+
cd /some/path
+MESOP_STATIC_FOLDER=assets mesop mesop-app/main.py
+
+

Enabling a static folder named "assets" and URL path of /assets

+

This will make the files in the assets directory accessible from the Mesop server +at /assets. For example: https://example.com/assets.

+
MESOP_STATIC_FOLDER=assets MESOP_STATIC_URL_PATH=/assets mesop main.py
+
+

Using a .env file

+

You can also specify the environment variables in a .env file. This file should be +placed in the same directory as the main.py file.

+
.env
MESOP_STATIC_FOLDER=assets
+MESOP_STATIC_URL_PATH=/assets
+
+

Then you can run the Mesop command like this:

+
mesop main.py
+
+

Example use cases

+

Here are a couple examples that use the static assets feature.

+ +

This example shows you how to load an image to use as a logo for your app.

+

Let's assume you have a directory like this:

+
    +
  • static/logo.png
  • +
  • main.py
  • +
  • requirements.txt
  • +
+

Then you can reference your logo in your Mesop app like this:

+
main.py
import mesop as me
+
+@me.page()
+def foo():
+  me.image(src="/static/logo.png")
+
+

Load a Tailwind stylesheet

+

This example shows you how to use Tailwind CSS with Mesop.

+

Let's assume you have a directory like this:

+
    +
  • static/tailwind.css
  • +
  • tailwind_input.css
  • +
  • tailwind.config.js
  • +
  • main.py
  • +
  • requirements.txt
  • +
+

You can import the CSS into your page using the stylesheets parameter on @me.page.

+
main.py
import mesop as me
+
+@me.page(stylesheets=["/static/tailwind.css"])
+def foo():
+  with me.box(classes="bg-gray-800"):
+    me.text("Mesop with Tailwind CSS.")
+
+

Tailwind is able to extract the CSS properties from your Mesop main.py file. This does +not work for all cases. If you are dynamically generating CSS properties using string concatenation/formatting, then Tailwind may not be able to determine which properties +to include. In that case, you may need to manually add these classes to the safelist.

+
tailwind.config.js
/** @type {import('tailwindcss').Config} */
+module.exports = {
+  content: ["main.py"],
+  theme: {
+    extend: {},
+  },
+  plugins: [],
+  safelist: [],
+};
+
+

This is just the base Tailwind input file.

+
tailwind_input.css
@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+

The command to generate the output Tailwind CSS is:

+
# This assumes you have the tailwindcss CLI installed. If not, see
+# https://tailwindcss.com/docs/installation
+npx tailwindcss -i ./tailwind_input.css -o ./static/tailwind.css
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/testing/index.html b/guides/testing/index.html new file mode 100644 index 000000000..d99172682 --- /dev/null +++ b/guides/testing/index.html @@ -0,0 +1,1739 @@ + + + + + + + + + + + + + + + + + + + + + + + Testing - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Testing

+

This guide covers the recommended approach for testing Mesop applications using Playwright, a popular browser automation and testing framework.

+

However, you can use any other testing framework and there's no official testing framework for Mesop.

+

Testing Philosophy

+

Because Mesop is a full-stack UI framework we recommend doing integration tests to cover critical app functionality. We also recommend separating core business logic into separate Python modules, which do not depend on Mesop. This way you can easily unit test your business logic as well as reuse the business logic as part of scripts or other binaries besides Mesop.

+

Playwright Example

+

We will walk through mesop-playwright-testing repo which contains a simple Mesop counter app and a Playwright test written in TypeScript (node.js). Although Playwright has a Python flavor, we recommend using the Playwright node.js flavor because it has better testing support. Even if you're not familiar with JavaScript or TypeScript, it's extremely easy to write Playwright tests because you can generate your tests by clicking through the UI.

+

The testing example repo's README.md contains instructions for setting up your environment and running the tests.

+

Playwright config

+

The playwright configuration used is similar to the default one, however we change a few configurations specific for Mesop.

+

For example, in playwright.config.ts, we configure Mesop as the local dev server:

+
  webServer: {
+    command: "mesop app.py",
+    url: "http://127.0.0.1:32123",
+    reuseExistingServer: !process.env.CI,
+  },
+
+

This will launch the Mesop app server at the start of the tests.

+

We also added the following configurations to make writing and debugging tests easier:

+
  use: {
+    /* Base URL to use in actions like `await page.goto('/')`. */
+    baseURL: "http://127.0.0.1:32123",
+
+    /* See https://playwright.dev/docs/trace-viewer */
+    trace: "retain-on-failure",
+
+    // Capture screenshot after each test failure.
+    screenshot: "on",
+
+    // Retain video on test failure.
+    video: "retain-on-failure",
+  },
+
+

Running and debugging a test

+

Try changing the test so that it fails. For example, in app.spec.ts change "Count=1" to "Count=2" and then run the tests: npx playwright test.

+

The test will fail (as expected) and a new browser page should be opened with the test failure information. You can click on the failing test and view the screenshot, video and trace. This is very helpful in figuring out why a test failed.

+

Writing a test

+

As mentioned above, it's very easy to write Playwright tests because you can generate your tests by clicking through the UI. Even if you're not familiar with JavaScript/TypeScript, you will be able to generate most of the test code by clicking through the UI and copying the generated test code.

+
+Use the Playwright VS Code extension +

You can use the Playwright VS Code extension to directly generate test code in your file. You can also run and debug tests from VS Code as well.

+
+

Learn more

+

We recommend reading Playwright's docs which are easy to follow and contain much more information on writing browser tests.

+

You can also look at Mesop's own tests written with Playwright.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/theming/index.html b/guides/theming/index.html new file mode 100644 index 000000000..88a5da506 --- /dev/null +++ b/guides/theming/index.html @@ -0,0 +1,2022 @@ + + + + + + + + + + + + + + + + + + + + + + + Theming - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Theming

+

Mesop has early-stage support for theming so you can support light theme and dark theme in a Mesop application.

+

Dark Theming

+

For an actual example of using Mesop's theming API to support light theme and dark theme, we will look at the labs chat component which itself is written all in Python built on top of lower-level Mesop components.

+

Theme toggle button

+

Inside the chat component, we've defined an icon button to toggle the theme so users can switch between light and dark theme:

+
def toggle_theme(e: me.ClickEvent):
+    if me.theme_brightness() == "light":
+      me.set_theme_mode("dark")
+    else:
+      me.set_theme_mode("light")
+
+with me.content_button(
+    type="icon",
+    style=me.Style(position="absolute", right=0),
+    on_click=toggle_theme,
+):
+    me.icon("light_mode" if me.theme_brightness() == "dark" else "dark_mode")
+
+

Using theme colors

+

You could define custom style logic to explicitly set the color based on the theme:

+
def container():
+  me.box(
+    style=me.Style(
+      background="white" if me.theme_brightness() == "light" else "black"
+    )
+  )
+
+

But this would be pretty tedious, so you can use theme variables like this:

+
def container():
+  me.box(style=me.Style(background=me.theme_var("background")))
+
+

This will use the appropriate background color for light theme and dark theme.

+

Default theme mode

+

Finally, we want to use the default theme mode to "system" which means we will use the user's preferences for whether they want dark theme or light theme. For many users, their operating systems will automatically switch to dark theme during night time.

+
+

Note: Mesop currently defaults to light theme mode but will eventually default to system theme mode in the future.

+
+

On our demo page with the chat component, we have a page on_load event handler defined like this:

+
def on_load(e: me.LoadEvent):
+  me.set_theme_mode("system")
+
+

Theme Density

+

You can set the visual density of the Material components. By default, Mesop uses the least visually dense setting, i.e.

+
me.set_theme_density(0) # 0 is the least dense
+
+

You can configure the density as an integer from 0 (least dense) to -4 (most dense). For example, if you want a medium-dense UI, you can do the following:

+
def on_load(e: me.LoadEvent):
+  me.set_theme_density(-2) # -2 is more dense than the default
+
+
+@me.page(on_load=on_load)
+def page():
+  ...
+
+

API

+ + +
+ + +

+ set_theme_density + +

+ + +
+ +

Sets the theme density for the Material components in the application. +A higher density (more negative value) results in a more compact UI layout.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
density +
+

The desired theme density. It can be 0 (least dense), + -1, -2, -3, or -4 (most dense).

+
+

+ + TYPE: + Literal[0, -1, -2, -3, -4] + +

+
+ +
+ +
+ +
+ + +

+ set_theme_mode + +

+ + +
+ +

Sets the theme mode for the application.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
theme_mode +
+

The desired theme mode. It can be "system", "light", or "dark".

+
+

+ + TYPE: + ThemeMode + +

+
+ +
+ +
+ +
+ + +

+ theme_brightness + +

+ + +
+ +

Returns the current theme brightness.

+

This function checks the current theme being used by the application +and returns whether it is "light" or "dark".

+ +
+ +
+ +
+ + +

+ theme_var + +

+ + +
+ +

Returns the CSS variable for a given theme variable.

+ + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
var +
+

The theme variable name. See the Material Design docs for more information about the colors available.

+
+

+ + TYPE: + ThemeVar + +

+
+ +
+ +
+ +
+ + + +

+ ThemeVar = Literal['background', 'error', 'error-container', 'inverse-on-surface', 'inverse-primary', 'inverse-surface', 'on-background', 'on-error', 'on-error-container', 'on-primary', 'on-primary-container', 'on-primary-fixed', 'on-primary-fixed-variant', 'on-secondary', 'on-secondary-container', 'on-secondary-fixed', 'on-secondary-fixed-variant', 'on-surface', 'on-surface-variant', 'on-tertiary', 'on-tertiary-container', 'on-tertiary-fixed', 'on-tertiary-fixed-variant', 'outline', 'outline-variant', 'primary', 'primary-container', 'primary-fixed', 'primary-fixed-dim', 'scrim', 'secondary', 'secondary-container', 'secondary-fixed', 'secondary-fixed-dim', 'shadow', 'surface', 'surface-bright', 'surface-container', 'surface-container-high', 'surface-container-highest', 'surface-container-low', 'surface-container-lowest', 'surface-dim', 'surface-tint', 'surface-variant', 'tertiary', 'tertiary-container', 'tertiary-fixed', 'tertiary-fixed-dim'] + + + module-attribute + + +

+ + +
+
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/guides/web-security/index.html b/guides/web-security/index.html new file mode 100644 index 000000000..fe52d2ebc --- /dev/null +++ b/guides/web-security/index.html @@ -0,0 +1,1709 @@ + + + + + + + + + + + + + + + + + + + + + + + Web Security - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Web Security

+

Static file serving

+

Mesop allows serving JS and CSS files located within the Mesop app's file subtree to support web components.

+

Security Warning: Do not place any sensitive or confidential JS and CSS files in your Mesop project directory. These files may be inadvertently exposed and served by the Mesop web server, potentially compromising your application's security.

+

JavaScript Security

+

At a high-level, Mesop is built on top of Angular which provides built-in security protections and Mesop configures a strict Content Security Policy.

+

Specifics:

+
    +
  • Mesop APIs do not allow arbitrary JavaScript execution in the main execution context. For example, the markdown component sanitizes the markdown content and removes active HTML content like JavaScript.
  • +
  • Mesop's default Content Security Policy prevents arbitrary JavaScript code from executing on the page unless it passes Angular's Trusted Types polices.
  • +
+

Iframe Security

+

To prevent clickjacking, Mesop apps, when running in prod mode (the default mode used when deployed), do not allow sites from any other origins to iframe the Mesop app.

+
+

Note: pages from the same origin as the Mesop app can always iframe the Mesop app.

+
+

If you want to allow a trusted site to iframe your Mesop app, you can explicitly allow list the sources which can iframe your app by configuring the security policy for a particular page.

+

Example

+
import mesop as me
+
+
+@me.page(
+  path="/allows_iframed",
+  security_policy=me.SecurityPolicy(
+    allowed_iframe_parents=["https://google.com"],
+  ),
+)
+def app():
+  me.text("Test CSP")
+
+

You can also use wildcards to allow-list multiple subdomains from the same site, such as: https://*.example.com.

+

API

+

You can configure the security policy at the page level. See SecurityPolicy on the Page API docs.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 000000000..77a02de78 --- /dev/null +++ b/index.html @@ -0,0 +1,1487 @@ + + + + + + + + + + + + + + + + + + + + + Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + + + + + + + + + + + + + +
+
+ + + + + + + +

Home

+ + + +
+
Rapidly build AI apps in Python
+
+ Create web apps without the complexity of frontend development.
+ Used at Google for rapid AI app development. +
+ +
+ +
+
+
+ Create your AI chat app in minutes +
+
+ +
+ +
+
+
+
+ + +
+
Python-native
+
+ Write your UI in idiomatic Python +
+
+ With Mesop, you can leverage your existing Python expertise to build UIs effortlessly. No new languages to learn - just write clean, idiomatic Python using familiar constructs like functions, loops, and conditionals. +
+
+ +
+
+
+
@me.stateclass
+class State:
+  val: str
+
+@me.page()
+def page():
+    state = me.state(State)
+    me.text(state.val)
+
+
+
+
@me.stateclass
+class State:
+  is_loaded: bool
+
+@me.page()
+def page():
+    if me.state(State).is_loaded:
+      me.text("Loaded")
+    else:
+      me.progress_spinner()
+
+
+
+
from time import sleep
+
+import mesop as me
+
+
+def generate_str():
+  yield "foo"
+  sleep(1)
+  yield "bar"
+
+
+@me.stateclass
+class State:
+  string: str = ""
+
+
+def button_click(action: me.ClickEvent):
+  state = me.state(State)
+  for val in generate_str():
+    state.string += val
+    yield
+
+
+@me.page(path="/streaming")
+def main():
+  state = me.state(State)
+  me.button("click", on_click=button_click)
+  me.text(text=f"{state.string}")
+
+
+
+
@me.content_component
+def scaffold(url: str):
+  with me.box():
+    menu(url=url)
+    me.slot()
+
+@me.component
+def menu(url: str):
+  ...
+
+def page1():
+  with scaffold(url="/page1"):
+    some_content(...)
+
+
+
+
+
+
Modern UI principles
+
+ Declarative UI that's easy to understand +
+
+ Mesop streamlines UI development with a declarative approach. Build expressive, maintainable interfaces using battle-tested patterns in Python. Say goodbye to complex imperative logic and hello to intuitive, clean code. +
+
+ +
@me.stateclass
+class State:
+    image_data: str
+    detections: list[Detection]
+
+
+@me.page()
+def object_detector():
+    state = me.state(State)
+
+    me.text("Real-time Object Detection", type="headline-4")
+    me.uploader(label="Upload an image", on_upload=on_image_upload)
+
+    if state.image_data:
+        me.image(src=f"data:image/jpeg;base64,{state.image_data}")
+
+    if state.detections:
+        me.text("Detected Objects:", type="headline-5")
+        for detection in state.detections:
+            detection_component(detection)
+
+def detection_component(detection):
+    me.text(f"{detection.obj}: {detection.confidence:.2f}")
+
+def on_image_upload(e: me.UploadEvent):
+    state = me.state(State)
+    state.image_data = base64.b64encode(e.file.read()).decode()
+    state.detections = detect_objects(e.file)
+
+
+
Building blocks
+
+ Jumpstart with ready-to-use components +
+
+ Mesop provides a versatile range of 30 components, from low-level building blocks to high-level, AI-focused components. This flexibility lets you rapidly prototype ML apps or build custom UIs, all within a single framework that adapts to your project's use case. + + + AI components +
e.g. chat, text to image
+
+ + Form components +
e,g. input, checkbox, radio
+
+ + Data display components +
e,g. table, plot
+
+
+
+ +
+
Build anything
+
+ Build any user interface you can imagine +
+
+With Mesop, you can build virtually any web-based user interface or application you can imagine. From quick prototypes to enterprise tools, Mesop provides the customizability to bring your ideas to life. +
+ +
+ +
+
Extensible
+
+ Seamlessly integrate JS with web components +
+
+ Get the best of both worlds with Mesop web components. Leverage Python's simplicity for core logic, while accessing the vast ecosystem of JS libraries +
+
+ +
+
+
+
from typing import Any, Callable
+
+import mesop.labs as mel
+
+
+@mel.web_component(path="./counter_component.js")
+def counter_component(
+  *,
+  value: int,
+  on_decrement: Callable[[mel.WebEvent], Any],
+  key: str | None = None,
+):
+  return mel.insert_web_component(
+    name="quickstart-counter-component",
+    key=key,
+    events={
+      "decrementEvent": on_decrement,
+    },
+    properties={
+      "value": value,
+    },
+  )
+
+
+
+
import {
+  LitElement,
+  html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+class CounterComponent extends LitElement {
+  static properties = {
+    value: {type: Number},
+    decrementEvent: {type: String},
+  };
+
+  constructor() {
+    super();
+    this.value = 0;
+    this.decrementEvent = '';
+  }
+
+  render() {
+    return html`
+      <div class="container">
+        <span>Value: ${this.value}</span>
+        <button id="decrement-btn" @click="${this._onDecrement}">
+          Decrement
+        </button>
+      </div>
+    `;
+  }
+
+  _onDecrement() {
+    this.dispatchEvent(
+      new MesopEvent(this.decrementEvent, {
+        value: this.value - 1,
+      }),
+    );
+  }
+}
+
+customElements.define('quickstart-counter-component', CounterComponent);
+
+
+
+
+
+
Simple deployment
+
+ Deploy your app and share in minutes +
+
+Mesop streamlines cloud deployment, enabling you to share your AI application with the world in minutes. With step-by-step guides for deploying to Google Cloud Run or any cloud service that takes a container, you can go from local development to production-ready deployment without wrestling with complex server setups. + + +
+ Google Cloud Run +
Free for small apps
+
+
+
+
+ +
+
Developer experience
+
+ Delightful developer experience +
+
+Mesop streamlines app development with features like hot reload and strong IDE support with static types, eliminating friction and boosting productivity. +
+
+
+ Instant hot reload +
+ +
+
+
+ IDE support with static types +
+ +
+
+ +
+
Community
+
+ See what others are saying +
+
+ Join developers around the world who are building AI apps in Mesop. +
+
+ +
+
+ +

Disclaimer

+

This is not an officially supported Google product.

+ + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/architecture/index.html b/internal/architecture/index.html new file mode 100644 index 000000000..e0d66712b --- /dev/null +++ b/internal/architecture/index.html @@ -0,0 +1,1613 @@ + + + + + + + + + + + + + + + + + + + + + + + Architecture - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Architecture

+

This doc is meant to provide an overview of how Mesop is structured as a framework. It's not necessary to know this information as a developer using Mesop, but if you're developing Mesop's codebase, then this is helpful in laying out the lay of the land.

+

At the heart of Mesop is two subsystems:

+ +

Terminology

+
    +
  • Downstream - This refers to the synced version of Mesop inside of Google ("google3 third-party"). Although almost all the code is shared between the open-source and internal version of Mesop, there's many considerations in maintaining parity between these two versions, particularly with regards to toolchain.
  • +
  • Component vs component instance - A component typically refers to the Python factory function that creates a component instance (e.g. me.box()). A component instance refers to a specific component created by a component function and is represented as a Component proto. Other UI frameworks oftentimes give a different name for an instance (i.e. Element) of a component, but for simplicity and explicitness, I will refer to these instances as component instance or component tree (for the entire tree of component instances) in this doc.
  • +
+

Life of a Mesop request

+

Initial page load

+

When a user visits a Mesop application, the following happens:

+
    +
  1. The user visits a path on the Mesop application, e.g. "/" (root path), in their browser.
  2. +
  3. The Mesop client-side web application (Angular) is bootstrapped and sends an InitRequest to the server.
  4. +
  5. The Mesop server responds with a RenderEvent which contains a fully instantiated component tree.
  6. +
  7. The Mesop client renders the component tree. Every Mesop component instance corresponds to 1 or more Angular component instance.
  8. +
+

User interactions

+

If the user interacts with the Mesop application (e.g. click a button), the following happens:

+
    +
  1. The user triggers a UserEvent which is sent to the server. The UserEvent includes: the application state (represented by the States proto), the event handler id to trigger, the key of the component interacted with (if any), and the payload value (e.g. for checkbox, it's a bool value which represents the checked state of the checkbox).
  2. +
  3. The server does the following:
      +
    1. Runs a first render loop in tracing mode (i.e. instantiate the component tree from the root component of the requested path). This discovers any event handler functions. In the future, this trace can also be used to calculate the before component tree so we can calculate the diff of the component tree to minimize the network payload.
    2. +
    3. Updates the state by feeding the user event to the event handler function discovered in the previous step.
      +

      Note: there's a mapping layer between the UserEvent proto and the granular Python event type. This provides a nicer API for Mesop developers then the internal proto representation.

      +
      +
    4. +
    5. Runs a second render loop to generate the new component tree given the new state. After the first render loop, each render loop results in a RenderEvent sent to the client.
    6. +
    7. In the streaming case, we may run the render loop and flush it down via Server-Sent Events many times.
    8. +
    +
  4. +
  5. The client re-renders the Angular application after receiving each RenderEvent.
  6. +
+

Python Server

+

Flask is a minimalist Python server framework that conforms to WSGI (Web Server Gateway Interface), which is a Python standard that makes it easy for web servers (oftentimes written in other languages like C++) to delegate requests to a Python web framework. This is particularly important in the downstream case because we rely on an internal HTTP server to serve Mesop applications.

+

For development purposes (i.e. using the CLI), we use Werkzeug, which is a WSGI library included with Flask.

+

Web Client

+

Mesop's Web client consists of three main parts:

+
    +
  • Core: Includes the root Angular component and singleton services like Channel. This part is fairly small and is the critical glue between the rest of the client layer and the server.
  • +
  • Mesop Components: Every Mesop component has its own directory under /components
    +

    Note: this includes both the Python API and the Angular implementation for developer convenience.

    +
    +
  • +
  • Dev Tools: Mesop also comes with a basic set of developer tools, namely the components and log panels. The components panel allows Mesop developers to visualize the component tree. The log panel allows Mesop developers to inspect the application state and component tree values.
  • +
+

Static assets

+
    +
  • Using the regular CLI, the web client static assets (i.e. JS binary, CSS, images) are served from the Python server. This simplifies deployment of Mesop applications by reducing version skew issues between the client and server.
  • +
  • In uncompiled mode (using the dev CLI), the web client is served from the web devserver. This is convenient because it builds faster than the regular/compiled mode and it allows live-reloading when developing the client codebase.
  • +
+

Tooling

+

Outside of the mesop/ directory are various tools used to build, test and document the Mesop framework. However, anything needed to actually run a Mesop application should be located within mesop/. The three main tools inside the codebase are:

+
    +
  • Build tooling - these are in build_defs/ which contains various Bazel bzl files and tools which is forked from the Angular codebase. The build toolchain is described in more detail on the toolchain doc.
  • +
  • Component generator - inside generator/ is a mini-library and CLI tool to generate Mesop components from existing Angular components, specifically Angular Material, although with some modifications it could support more generic Angular components. The generator modifies the codebase so that nothing in generator/ is actually needed when running a Mesop applications.
  • +
  • Docs - Mesop's doc site is built using Material for Mkdocs and is what you are looking at right now.
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/ci/index.html b/internal/ci/index.html new file mode 100644 index 000000000..f9e342c62 --- /dev/null +++ b/internal/ci/index.html @@ -0,0 +1,1359 @@ + + + + + + + + + + + + + + + + + + + + + + + CI - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

CI

+

We use GitHub actions. For all third-party GitHub actions, we must pin it to a specific hash to comply with internal policies.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/codespaces/index.html b/internal/codespaces/index.html new file mode 100644 index 000000000..150707838 --- /dev/null +++ b/internal/codespaces/index.html @@ -0,0 +1,1523 @@ + + + + + + + + + + + + + + + + + + + + + + + Github Codespaces - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Development on Github Codespaces

+

Github Codespaces is a quick way to get +started with internal Mesop development. All you need to do is a click a button and a +fully configured workspace will be created for you. No need to spend time debugging +installation issues.

+

Github Free and Pro plans also provide a free tier, +so Codespaces is useful for writing and testing quick patches.

+
+

If using the free tier, the Codespace setup takes 20-30 minutes due to the limited +CPU available.

+
+

Create Github Codespace

+

You can create a Github Codespace from the Mesop Github repository page.

+

Create Github Codespace

+

Wait for postCreateCommand to run

+

The Codespace will not be usable until the postCreateCommand has completed. You can +view the CLI output by pressing Cmd/Ctrl + Shift + P and then finding the View +Creation Log option.

+

Create Github Codespace

+

Set the Python environment for the Codespace

+

During the postCreateCommand step, you'll see a pop up asking if you want to set a new +environment for the Codespace. Select Yes here to use the virtual env that is created +as part of the postCreateCommand set up.

+

Set Python environment

+

Run Mesop for development

+

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

+
./scripts/cli.sh
+
+

This step takes some time for the first run.

+

You will see some warning messages, but it is OK to ignore them. You can also ignore the +message shown in the screenshot.

+

CLI message to ignore

+

View Mesop demos

+

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos from +the PORTS tab.

+

View Mesop demos from PORTS panel

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/contributing/index.html b/internal/contributing/index.html new file mode 100644 index 000000000..467227fdb --- /dev/null +++ b/internal/contributing/index.html @@ -0,0 +1,1550 @@ + + + + + + + + + + + + + + + + + + + + + + + How-to Contribute - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

How-to Contribute

+

Thanks for looking into contributing to Mesop. There's many ways to contribute to Mesop:

+
    +
  • Filing new issues and providing feedback on existing issues
  • +
  • Improving our docs
  • +
  • Contributing examples
  • +
  • Contributing code
  • +
+

All types of contributions are welcome and are a key piece to making Mesop work well as a project.

+

Before you begin

+

Sign our Contributor License Agreement

+

Contributions to this project must be accompanied by a +Contributor License Agreement (CLA). +You (or your employer) retain the copyright to your contribution; this simply +gives us permission to use and redistribute your contributions as part of the +project.

+

If you or your current employer have already signed the Google CLA (even if it +was for a different project), you probably don't need to do it again.

+

Visit https://cla.developers.google.com/ to see your current agreements or to +sign a new one.

+

Review our community guidelines

+

This project follows +Google's Open Source Community Guidelines.

+

Contributing to docs

+

If you want to contribute to our docs, please take a look at our docs issues. If you find any of our existing docs confusing or missing key information, please file an issue and we will see how we can improve things. We regularly spend time improving our docs because we know it's a key part of the developer experience.

+

Contributing examples

+

One of the best way of helping the Mesop project is to share what you've built! You can either add an example to our demo gallery by adding it to the demo/ directory or you can send us a link to your app running and we will include it in our docs.

+

Contributing code

+

If you'd like to contribute code, I recommend taking a look at one of our existing "starter" issues. These are issues that are good for first-time contributors as they are well-specified.

+ +

I recommend reading through the various pages in the contributing section as it will give you a sense of our project's goals.

+

One thing that we focus on is providing an easy-to-understand API with minimal breaking changes so we ask that any API changes are first discussed in an issue. This will help prevent wasted work because we are conservative with changing our APIs.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/development/index.html b/internal/development/index.html new file mode 100644 index 000000000..b81f43715 --- /dev/null +++ b/internal/development/index.html @@ -0,0 +1,1662 @@ + + + + + + + + + + + + + + + + + + + + + + + Main - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Development

+

I recommend following (or at least reading) all the steps in this doc if you plan on actively developing Mesop.

+

Setup

+

Bazel/ibazel

+

We use Bazel as our build system. Use bazelisk which ensures the right version of Bazel is used for this project.

+

If ibazel breaks, but bazel works, try following these steps

+
+

TIP: If your build mysteriously fails due to an npm-related error, try running bazel clean --expunge && rm -rf node_modules. Bazel and Yarn have a cache bug when upgrading npm modules.

+
+

uv

+

We use uv. Follow the instructions here to install uv.

+

Commit hooks

+
    +
  1. Install pre-commit
  2. +
  3. Install pre-commit hooks for this repo: pre-commit install
  4. +
+

Run local development

+

We recommend using this for most Mesop framework development.

+
./scripts/cli.sh
+
+
+

NOTE: this automatically serves the angular app.

+
+

Python

+

Third-party packages (PIP)

+

If you update //build_defs/requirements.txt, run:

+
bazel run //build_defs:pip_requirements.update
+
+

venv

+

To support IDE type-checking (Pylance) in VS Code, we use Aspect's rules_py which generates a venv target.

+
bazel run //mesop/cli:cli.venv
+
+

Then, you can activate the venv:

+
source .cli.venv/bin/activate
+
+

You will need to setup a symlink to have Python IDE support for protos:

+
./scripts/setup_proto_py_modules.sh
+
+

Check that you're using venv's python:

+
which python
+
+

Copy the python interpreter path and paste it into VS Code.

+

Finally, install third-party dependencies.

+
pip install -r build_defs/requirements_lock.txt
+
+
+

NOTE: You may need to run the command with sudo if you get a permission denied error, particularly with "_distutils_hack".

+
+

Commit hooks

+

We use pre-commit to automatically format, lint code before committing.

+

Setup:

+
    +
  1. Install pre-commit.
  2. +
  3. Setup git hook: pre-commit install
  4. +
+

Docs

+

We use Mkdocs Material to generate our docs site.

+
    +
  1. Activate venv
  2. +
  3. mkdocs serve
  4. +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/hot-reload/index.html b/internal/hot-reload/index.html new file mode 100644 index 000000000..b7a32e17c --- /dev/null +++ b/internal/hot-reload/index.html @@ -0,0 +1,1516 @@ + + + + + + + + + + + + + + + + + + + + + + + Hot reload - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Hot Reload

+

One of Mesop's key benefits is that it provides a fast iteration cycle through hot reload. This means whenever a Mesop developer changes their Mesop app code, their browser window will automaticall reload and execute the new app code while preserving the existing state. This isn't guaranteed to work, for example, if the State class is modified in an incompatible way, but it should work for >90% of the build-edit loops (e.g. tweaking the UI, calling new components).

+

How it works

+

See: https://github.com/google/mesop/pull/211

+

Design decisions

+

What to reload

+

Right now we reload all the modules loaded by the Mesop application. However, this results in a lot of unnecessary modules being reloaded and can be quite slow if there's a heavy set of transitive dependencies.

+

Instead, I'm thinking we can use a heuristic where we calculate the existing package based on the file path passed in and only reload modules which are in the current package or a sub-package. Effectively this is only reloading modules within the target file's subtree.

+

This seems like a pretty reasonable heuristic where it reloads all the application modules without reloading the entire dependency graph. Previously I tried reloading only the module passed in via --path, however this was too limiting as it meant shared code (e.g. a navmenu) would not get hot-reloaded.

+

When to reload

+

With the previous design decision, re-executing a module should be much faster, but we still need to guard against the case where the live reload occurs too quickly in the client side. Options:

+
    +
  • Wait a fixed timeout - A simple heuristic could just be to wait 500ms since in theory, all the application code (with the non-application dependnecies cached) should re-execute fairly quickly.
  • +
  • Client retry/reload - Another approach could be to retry a client-side reload N times (e.g. 3) if we get an error. The pattern could be: 1. save state to local storage, 2. trigger reload, 3. if reload results in a successful render, we clear the state OR if reload results in an error, we trigger a reload (and persist in local storage which retry attempt this is).
  • +
  • Server loop - In the common error case where the server is still re-executing the module and the client reloads, it will hit path not found because the path hasn't been registered yet. One way of mitigating this is to simply do a sleep in debug mode. We can even do an exponential backoff for the sleep (e.g. wait 300ms, 900ms, 2700ms).
  • +
  • Preferred appproach - given the trade-offs, I think Server loop is the best option as it's relatively simple to implement, robust and doesn't incur a significant delay in the happy case.
  • +
+

Abstracting ibazel-specific details

+

Since Google's internal equivalent of ibazel doesn't work exactly the same, we should treat HotReloadService as an abstract base class and then extend it for Ibazel (and the internal variant).

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/modes/index.html b/internal/modes/index.html new file mode 100644 index 000000000..98fa1f04d --- /dev/null +++ b/internal/modes/index.html @@ -0,0 +1,1447 @@ + + + + + + + + + + + + + + + + + + + + + + + Modes - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Modes

+

There are two modes that you can run Mesop in.

+

Development mode (aka debug mode or editor mode)

+

Recommended for developers using Mesop when they are developing the apps locally. This provides good error messages and hot reloading.

+
    +
  • How to run: ibazel run //mesop/cli -- --path=mesop/mesop/example_index.py
  • +
  • Angular should run in dev mode.
  • +
  • Developer Tools and Visual Editor are available.
  • +
+

Prod mode

+

Recommended when developers deploy applications built with Mesop for public serving. This is optimized for performance and provides less-detailed error messages.

+
    +
  • Developer tools aren't available.
  • +
  • Angular doesn't run in dev mode.
  • +
  • How to run: bazel run //mesop/cli -- --path=mesop/mesop/example_index.py --prod
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/new-component/index.html b/internal/new-component/index.html new file mode 100644 index 000000000..0d0d9055f --- /dev/null +++ b/internal/new-component/index.html @@ -0,0 +1,1487 @@ + + + + + + + + + + + + + + + + + + + + + + + New component - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

New Component

+

How-to

+
python scripts/scaffold_component.py $component_name
+
+

API Guidelines

+
    +
  • Make all arguments keyword only by putting * as the initial argument. Keyword argument is more readable, particularly for UI components which will have increasingly more optional arguments over time.
  • +
  • Model after existing APIs. For example, if we are wrapping an existing @angular/material component, we will try to mirror their API (within reason). If we are wrapping a native HTML element, we should try to expose a similar API. In some cases, we will look at other UI frameworks like Flutter for inspiration, even though we are not directly wrapping them.
  • +
  • Prefer small components. We should try to provide small native components that can be composed by content components in Python. This enables a wider range of use cases.
  • +
+

New events

+

Try to reuse the existing events when possible, but you may need to sometimes create a new event.

+
    +
  1. Define the event class in //mesop/events/{event_name}.py
  2. +
  3. In the same file, define an event mapper and register it: runtime().register_event(EventClass, event_mapper)
  4. +
+

Potential exploration areas

+
    +
  • Code-gen component_renderer using a shell/Python script. Initially, just run the script as-needed, but eventually can run it as part of a BUILD rule (a la @angular/components examples)
  • +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/publishing/index.html b/internal/publishing/index.html new file mode 100644 index 000000000..2db0b78be --- /dev/null +++ b/internal/publishing/index.html @@ -0,0 +1,1677 @@ + + + + + + + + + + + + + + + + + + + + + Publishing - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

Publishing

+

Follow these instructions for releasing a new version of Mesop publicly via PyPI (e.g. pip install mesop).

+

If you haven't done this before, follow the first-time setup.

+

Check main branch

+

Before, cutting a release, you'll want to check two things:

+
    +
  1. The main branch should be healthy (e.g. latest commit is green).
  2. +
  3. Check the snyk dashboard to review security issues: + - It only runs weekly so you need to click "Retest now". If there's any High security issues for a core Mesop file (e.g. anything in mesop/*), then you should address it before publishing a release.
  4. +
+

Update version to RC

+

Update mesop/version.py by incrementing the version number. We follow semver.

+

You want to first create an RC (release candidate) to ensure that it works.

+

For example, if the current version is: 0.7.0, then you should increment the version to 0.8.0rc1 which will create an RC, which is treated as a pre-release by PyPI.

+

Install locally

+

From the workspace root, run the following command:

+
source ./scripts/pip.sh
+
+

This will build the Mesop pip package and install it locally so you can test it.

+

Testing locally

+
+

TIP: Double check the Mesop version is expected. It's easy to use the wrong version of Mesop by loading mesop or gunicorn from a different Python path (i.e. not the venv you just created).

+
+

Dev CLI

+

The above shell script will tell you to run the following command:

+
mesop main.py
+
+

This will start the Mesop dev server and you can test that hot reload works.

+

Gunicorn integration

+
gunicorn main:me
+
+
+

Note: gunicorn should already be installed by the shell script above.

+
+

Upload to PyPI

+

If the testing above looks good, then continue with uploading to PyPI.

+
rm -rf /tmp/mesoprelease-test/venv-twine \
+&& virtualenv --python python3 /tmp/mesoprelease-test/venv-twine \
+&& source /tmp/mesoprelease-test/venv-twine/bin/activate \
+&& pip install --upgrade pip \
+&& pip install twine \
+&& cd /tmp/mesoprelease-test \
+&& twine upload mesop*.whl
+
+

Visit https://pypi.org/project/mesop/ to see that the new version has been published.

+

Test on Colab

+

Because Colab installs from PyPI, you will need to test the RC on Colab after uploading to PyPI.

+

Open our Mesop Colab notebook. You will need to explicitly pip install the RC version as pip will not automatically install a pre-release version, even if it's the newest version. So change the first cell to something like:

+
 !pip install mesop==0.X.Yrc1
+
+
+

Tip: sometimes it takes a minute for the PyPI registry to be updated after upload, so just try again.

+
+

Then, run all the cells and make sure it works. Usually if something breaks in Colab, it's pretty obvious because the output isn't displayed, etc.

+

Change the version from RC to regular release

+

If you find an issue, then redo the above steps and create another RC candidate: 0.8.0rc1 -> 0.8.0rc2.

+

If all the testing looks good, then you can update mesop/version.py and change the version from RC to a regular release, for example:

+

0.8.0rc1 -> 0.8.0

+

Re-do the steps above to build, test and upload it to PyPI.

+

Publish GitHub release

+

After you've uploaded a new regular release to PyPI, submit the PR which bumps the version and then publish a GitHub release.

+
    +
  1. Click "Choose a tag" and type in the version you just released. This will create a new Git tag.
  2. +
  3. Click "Genereate release notes".
  4. +
  5. Click "Create a discussion for this release".
  6. +
  7. Click "Publish release".
  8. +
+

First-time upload setup

+

Create a file ~/.pypirc:

+
[pypi]
+  username = __token__
+  password = {{password}}
+
+

You will need to get a PyPI token generated by one of the project maintainers.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/testing/index.html b/internal/testing/index.html new file mode 100644 index 000000000..00b7a39b1 --- /dev/null +++ b/internal/testing/index.html @@ -0,0 +1,1523 @@ + + + + + + + + + + + + + + + + + + + + + + + Testing - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Testing

+

Unit tests

+

You can run our unit tests using Bazel.

+

Run tests

+
bazel test //mesop/...
+
+

E2E tests

+

We use Playwright as our e2e test framework. Unlike most of the stack, this isn't Bazel-ified although we'd like to eventually do this.

+

Run tests

+
yarn playwright test
+
+

Debug tests

+
yarn playwright test --debug
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/toolchain/index.html b/internal/toolchain/index.html new file mode 100644 index 000000000..033c963c2 --- /dev/null +++ b/internal/toolchain/index.html @@ -0,0 +1,1496 @@ + + + + + + + + + + + + + + + + + + + + + + + Build / Toolchain - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Build / Toolchain

+

Context

+

Because Mesop is a Google open-source project and we want to provide a good integration with Google's internal codebase, Mesop uses Google's build system Bazel.

+

Although Bazel is similar to the internal tool, there's numerous differences, particularly around the ecosystems, which makes it quite a challenge to maintain Mesop for both open-source and internal builds. Nevertheless, it's important that we do this to serve both communities well.

+

Differences

+

We try to isolate as much of the differences between these two environments into the build_defs/ directory. Different versions of the same files inside build_defs/ are maintained for each environment. In particular, build_defs/defaults.bzl is meant to wrap all external rules/macros used by Mesop so we can swap it between the internal and external variants as needed.

+

Finally, all external dependencies, e.g. Python's requirement('$package') or NPM's @npm//$package, are referenced via an indirection to build_defs/defaults.bzl. This is because Google has a special approach to handling third-party dependencies.

+

Gotchas

+

Here's a quick list of gotchas to watch out for:

+
    +
  • Do not use import * as when importing protos from TS. This prevents tree-shaking downstream.
  • +
  • Do not use any external Bazel references (e.g. @) within mesop/. Instead, reference them indirectly using a wrapper in build_defs/.
  • +
  • Avoid relying on implicit transitive dependencies, particularly for TS/NG modules.
  • +
  • Do not use raw JSON.parse, instead use jsonParse in strict_types.ts.
  • +
+

Angular

+

We rely heavily on Angular's toolchain, particularly around Bazel integration. Many of the Web-related Bazel rules, particularly for Angular/TS code was forked from github.com/angular/components.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/type-checking/index.html b/internal/type-checking/index.html new file mode 100644 index 000000000..1c563ff93 --- /dev/null +++ b/internal/type-checking/index.html @@ -0,0 +1,1420 @@ + + + + + + + + + + + + + + + + + + + + + + + Type checking - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Type Checking

+

Python Type Checking

+

For our Python code, we use pyright as our static type checker because it has excellent IDE support in VS Code via pylance.

+

To run Python type-checking, run:

+
./scripts/run_py_typecheck.sh
+
+

This will setup the pre-requisites needed for type-checking.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/internal/vs-code-remote-container/index.html b/internal/vs-code-remote-container/index.html new file mode 100644 index 000000000..65882070a --- /dev/null +++ b/internal/vs-code-remote-container/index.html @@ -0,0 +1,1569 @@ + + + + + + + + + + + + + + + + + + + + + + + VS Code Remote Container - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + + + + + +
+
+ + + + + + + +

VS Code Remote Container

+

VS Code Remote Containers is a quick way to get started with internal Mesop +development if you have VS Code and +Docker Desktop installed.

+

This approach will create a fully configured workspace, saving you time from +debugging installation issues and allowing you to start development right away.

+

Pre-requistes: Install VS Code and Docker

+

In order to use VS Code remote containers, you will need VS Code installed. You will +also need Docker Desktop (which will install Docker Engine and Docker Compose) to run +the remote containers.

+ +

Fork and Clone the Mesop repository

+
+

It is not recommended to develop locally and on remote containers using the same +folder since this may cause unexpected conflicts. Instead you should clone the +repository in a separate directory.

+
+

You can follow the instructions here on how to fork and clone a Github repository.

+

Share Git credentials with your container

+

The VS Code Dev Containers extension provides a few ways to share your local Git +credentials with your remote container.

+

If you cloned the Mesop repo using HTTPS, you can use a Github CLI or Git Credential Manager.

+

If you used SSH, then your local ssh agent will automatically be forwarded into your +remote container. All you need do is run the ssh-add command to add the ssh key you've +configured for GitHub access.

+

See the Sharing Git credentials with your container page for full details.

+

Open folder in container

+

Open VS Code, press Cmd/Ctrl + Shift + P, and select the Dev Containers: Open Folder in Container... +option. This will create a new workspace inside a remote container.

+

VS Code open folder in container

+

Wait for postCreateCommand to run

+

The workspace will not be usable until the postCreateCommand has completed.

+

Post Create Command

+

Run Mesop for development

+

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

+
./scripts/cli.sh
+
+

You will see some warning messages, but it is OK to ignore them.

+

You should see this message once the Mesop server is ready.

+

Server started

+

View Mesop demos

+

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos at +http://localhost:32123.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/javascript/demo.js b/javascript/demo.js new file mode 100644 index 000000000..c14b7a866 --- /dev/null +++ b/javascript/demo.js @@ -0,0 +1,7 @@ +const urlParams = new URLSearchParams(window.location.search); +const demoParam = urlParams.get('demo') || ''; + +const iframe = document.createElement('iframe'); +iframe.src = 'https://wwwillchen-mesop.hf.space/' + demoParam; +iframe.className = 'full-screen-iframe'; +document.body.appendChild(iframe); diff --git a/javascript/docbot.js b/javascript/docbot.js new file mode 100644 index 000000000..d64fb2d40 --- /dev/null +++ b/javascript/docbot.js @@ -0,0 +1,124 @@ +document.body.setAttribute('tabindex', '-1'); + +// Function to show the docbot iframe +function showDocbotIframe() { + const iframe = document.getElementById('docbot-iframe'); + + iframe.style.display = 'block'; + console.log('focusIframe'); + iframe.focus(); + iframe.contentWindow.postMessage('focus', '*'); + + // Add event listener to close iframe when clicking outside the iframe + document.addEventListener('click', closeIframe); +} + +function closeIframe(e) { + const iframe = document.getElementById('docbot-iframe'); + if (!iframe.contains(e.target)) { + closeDocbot(); + } +} +// Listen for click events on the search input +document.addEventListener( + 'click', + function (event) { + console.log('event.target', event.target); + if (event.target.matches('[data-md-toggle="search"]')) { + event.preventDefault(); // Prevent default search behavior + event.stopPropagation(); + showDocbotIframe(); + } + if (event.target.matches('.md-search__input')) { + event.preventDefault(); // Prevent default search behavior + event.stopPropagation(); + showDocbotIframe(); + } + }, + // Added 'true' for event capture so we can intercept before mkdocs material + // handles the search button clicks. + true, +); + +document.addEventListener( + 'keydown', + function (event) { + console.log('keydown', event); + // Check if the Escape key is pressed + if (event.key === 'Escape') { + const iframe = document.getElementById('docbot-iframe'); + if (iframe.style.display === 'block') { + closeDocbot(); + } + return; + } + // Check if Command (Mac) or Control (Windows) + K is pressed + if ((event.metaKey || event.ctrlKey) && event.key === 'k') { + event.preventDefault(); + const iframe = document.getElementById('docbot-iframe'); + if (iframe.style.display === 'block') { + closeDocbot(); + return; + } + showDocbotIframe(); + } + }, + true, +); + +// Create the iframe +function createDocbotIframe() { + const iframe = document.createElement('iframe'); + iframe.id = 'docbot-iframe'; + iframe.src = ['http://localhost:8000', 'http://127.0.0.1:8000'].includes( + window.location.origin, + ) + ? 'http://localhost:32123' + : 'https://wwwillchen-mesop-docs-bot.hf.space/'; + iframe.style.position = 'fixed'; + iframe.style.display = 'none'; + iframe.style.top = '50%'; + iframe.style.left = '50%'; + iframe.style.transform = 'translate(-50%, -50%)'; + iframe.style.width = '80%'; + iframe.style.maxWidth = '720px'; + iframe.style.maxHeight = '600px'; + iframe.style.height = '80%'; + iframe.style.border = 'none'; + iframe.style.borderRadius = '16px'; + iframe.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)'; + iframe.style.zIndex = '9999'; + + // Append the iframe to the body + document.body.appendChild(iframe); + + return iframe; +} + +createDocbotIframe(); + +// Listen for 'message' events from the docbot iframe +window.addEventListener('message', function (event) { + if (event.data === 'closeDocbot') { + closeDocbot(); + } +}); + +function closeDocbot() { + const iframe = document.getElementById('docbot-iframe'); + iframe.style.display = 'none'; + // Put focus back on the body so keyboard shortcuts work + document.body.focus(); + + // This is a hack because the iframe steals focus. I believe + // it's because we reload the iframe below. + setTimeout(() => { + if (document.activeElement === iframe) { + document.body.focus(); + } + }, 500); + + // Reload the iframe + iframe.src = iframe.src; + document.removeEventListener('click', closeIframe); +} diff --git a/javascript/showcase.js b/javascript/showcase.js new file mode 100644 index 000000000..600790f2b --- /dev/null +++ b/javascript/showcase.js @@ -0,0 +1 @@ +window.location.href = 'https://wwwillchen-mesop-showcase.hf.space/'; diff --git a/objects.inv b/objects.inv new file mode 100644 index 000000000..5f2040a94 Binary files /dev/null and b/objects.inv differ diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 000000000..7e425c6e7 --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"Rapidly build AI apps in Python Create web apps without the complexity of frontend development. Used at Google for rapid AI app development. Get started View showcase Create your AI chat app in minutes Open in new tab Python-native Write your UI in idiomatic Python With Mesop, you can leverage your existing Python expertise to build UIs effortlessly. No new languages to learn - just write clean, idiomatic Python using familiar constructs like functions, loops, and conditionals. State managementControl flowStreaming UIComposable
@me.stateclass\nclass State:\n  val: str\n\n@me.page()\ndef page():\n    state = me.state(State)\n    me.text(state.val)\n
@me.stateclass\nclass State:\n  is_loaded: bool\n\n@me.page()\ndef page():\n    if me.state(State).is_loaded:\n      me.text(\"Loaded\")\n    else:\n      me.progress_spinner()\n
from time import sleep\n\nimport mesop as me\n\n\ndef generate_str():\n  yield \"foo\"\n  sleep(1)\n  yield \"bar\"\n\n\n@me.stateclass\nclass State:\n  string: str = \"\"\n\n\ndef button_click(action: me.ClickEvent):\n  state = me.state(State)\n  for val in generate_str():\n    state.string += val\n    yield\n\n\n@me.page(path=\"/streaming\")\ndef main():\n  state = me.state(State)\n  me.button(\"click\", on_click=button_click)\n  me.text(text=f\"{state.string}\")\n
@me.content_component\ndef scaffold(url: str):\n  with me.box():\n    menu(url=url)\n    me.slot()\n\n@me.component\ndef menu(url: str):\n  ...\n\ndef page1():\n  with scaffold(url=\"/page1\"):\n    some_content(...)\n
Modern UI principles Declarative UI that's easy to understand Mesop streamlines UI development with a declarative approach. Build expressive, maintainable interfaces using battle-tested patterns in Python. Say goodbye to complex imperative logic and hello to intuitive, clean code.
@me.stateclass\nclass State:\n    image_data: str\n    detections: list[Detection]\n\n\n@me.page()\ndef object_detector():\n    state = me.state(State)\n\n    me.text(\"Real-time Object Detection\", type=\"headline-4\")\n    me.uploader(label=\"Upload an image\", on_upload=on_image_upload)\n\n    if state.image_data:\n        me.image(src=f\"data:image/jpeg;base64,{state.image_data}\")\n\n    if state.detections:\n        me.text(\"Detected Objects:\", type=\"headline-5\")\n        for detection in state.detections:\n            detection_component(detection)\n\ndef detection_component(detection):\n    me.text(f\"{detection.obj}: {detection.confidence:.2f}\")\n\ndef on_image_upload(e: me.UploadEvent):\n    state = me.state(State)\n    state.image_data = base64.b64encode(e.file.read()).decode()\n    state.detections = detect_objects(e.file)\n
Building blocks Jumpstart with ready-to-use components Mesop provides a versatile range of 30 components, from low-level building blocks to high-level, AI-focused components. This flexibility lets you rapidly prototype ML apps or build custom UIs, all within a single framework that adapts to your project's use case. AI components e.g. chat, text to image Form components e,g. input, checkbox, radio Data display components e,g. table, plot Build anything Build any user interface you can imagine With Mesop, you can build virtually any web-based user interface or application you can imagine. From quick prototypes to enterprise tools, Mesop provides the customizability to bring your ideas to life. Extensible Seamlessly integrate JS with web components Get the best of both worlds with Mesop web components. Leverage Python's simplicity for core logic, while accessing the vast ecosystem of JS libraries counter_component.pycounter_component.js
from typing import Any, Callable\n\nimport mesop.labs as mel\n\n\n@mel.web_component(path=\"./counter_component.js\")\ndef counter_component(\n  *,\n  value: int,\n  on_decrement: Callable[[mel.WebEvent], Any],\n  key: str | None = None,\n):\n  return mel.insert_web_component(\n    name=\"quickstart-counter-component\",\n    key=key,\n    events={\n      \"decrementEvent\": on_decrement,\n    },\n    properties={\n      \"value\": value,\n    },\n  )\n
import {\n  LitElement,\n  html,\n} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';\n\nclass CounterComponent extends LitElement {\n  static properties = {\n    value: {type: Number},\n    decrementEvent: {type: String},\n  };\n\n  constructor() {\n    super();\n    this.value = 0;\n    this.decrementEvent = '';\n  }\n\n  render() {\n    return html`\n      <div class=\"container\">\n        <span>Value: ${this.value}</span>\n        <button id=\"decrement-btn\" @click=\"${this._onDecrement}\">\n          Decrement\n        </button>\n      </div>\n    `;\n  }\n\n  _onDecrement() {\n    this.dispatchEvent(\n      new MesopEvent(this.decrementEvent, {\n        value: this.value - 1,\n      }),\n    );\n  }\n}\n\ncustomElements.define('quickstart-counter-component', CounterComponent);\n
Simple deployment Deploy your app and share in minutes Mesop streamlines cloud deployment, enabling you to share your AI application with the world in minutes. With step-by-step guides for deploying to Google Cloud Run or any cloud service that takes a container, you can go from local development to production-ready deployment without wrestling with complex server setups. Google Cloud Run Free for small apps Developer experience Delightful developer experience Mesop streamlines app development with features like hot reload and strong IDE support with static types, eliminating friction and boosting productivity. Instant hot reload IDE support with static types Community See what others are saying Join developers around the world who are building AI apps in Mesop.

is this the thing that will finally save me from ever learning front end?https://t.co/eDgY0AfG6U

\u2014 xlr8harder (@xlr8harder) June 6, 2024

i hate writing frontend code, but can\u2019t resist a sleek UI. just tried Google\u2019s quietly released Mesop\u2014what a find! no frontend fuss, just python. if you value your sanity and good design, you should def try it. gives a balance between streamlit and gradiomesop docs :\u2026 pic.twitter.com/SmBAH5Leri

\u2014 Sanchay Thalnerkar (@7anchay) June 23, 2024

New Python-based UI framework in town - have you tried Mesop from Google?I gave it a spin, here's what I found out:\ud83d\udccc Provides a unique approach to building web hashtag#UIs in hashtag#Python with component-based architecture for customized UIs by taking inspiration from\u2026

\u2014 Harshit Tyagi (@dswharshit) June 11, 2024"},{"location":"#disclaimer","title":"Disclaimer","text":"

This is not an officially supported Google product.

"},{"location":"comparison/","title":"Comparison with Other Python UI Frameworks","text":"

This page aims to provide an objective comparison between Mesop and other popular Python-based web application frameworks, specifically Streamlit and Gradio. This is a difficult doc to write but we feel that it's important to explain the differences as this is frequently asked.

While we believe Mesop offers a unique philosophy for building UIs, we strive to be fair and accurate in highlighting the strengths of each framework.

Because this is a fast-moving space, some of the information may be out of date. Please file an issue and let us know what we should fix.

"},{"location":"comparison/#streamlit","title":"Streamlit","text":"

Streamlit and Mesop share similar goals in terms of enabling Python developers to rapidly build web apps, particularly for AI use cases.

However, there are some key differences:

"},{"location":"comparison/#execution-model","title":"Execution Model","text":"

Streamlit executes apps in a script-like manner where the entire app reruns on each user interaction. This enables a boilerplate-free UI development model that's easy to get started with, but requires mechanisms like caching and fragments to optimize the performance with this model.

Mesop uses a function-based model commonly found in web frameworks where the program is executed once on server initialization and then the page and component functions are executed in each render loop. This provides regular Python execution semantics because top-level initialization code is executed exactly once.

"},{"location":"comparison/#styling-and-customization","title":"Styling and Customization","text":"

Streamlit offers pre-styled components with customization primarily through themes, prioritizing consistency and ease of use over flexibility.

In addition to providing Material-themed components, Mesop offers a low-level Style API to configure CSS properties. Mesop provides limited theming support with dark theming and doesn't support theming to other colors.

"},{"location":"comparison/#components","title":"Components","text":"

Both Streamlit and Mesop offer a range of standard components (e.g., forms, tables, chat interfaces), with Streamlit providing a larger set of built-in components, especially for data science use cases like data visualization.

Streamlit supports custom components rendered in iframes for isolation. It offers first-class support for React components and can accommodate other frameworks through a framework-agnostic template.

Mesop enables creating custom web components based on open web standards, facilitating interoperability with components from different frameworks like Lit. Mesop web components are rendered in the same frame as the rest of the Mesop app which provides more flexibility but less isolation.

Streamlit has a more established ecosystem of community-developed components, while Mesop's community and component ecosystem are still developing.

"},{"location":"comparison/#gradio","title":"Gradio","text":"

Gradio and Mesop both enable rapid ML/AI app development but with different approaches.

Gradio has a strong focus on creating demos and interfaces for machine learning models and makes it easy to build a UI for a model. Gradio also offers a lower-level abstraction known as Blocks for more general web applications.

Mesop, while well-suited for ML/AI use cases, is a more general-purpose framework that can be used for a wide range of web applications.

"},{"location":"comparison/#components_1","title":"Components","text":"

Gradio provides a set of pre-built components optimized for common ML inputs and outputs (e.g. image classification, text generation). This makes it fast to set up standard model interfaces. In addition to built-in components, you can create custom components in Python and JavaScript (Svelte).

Mesop provides general-purpose UI components, which can be used for a variety of layout and UI designs. Higher-level components like the chat component are built on top of these low-level UI components. This makes it better suited for building custom interfaces, such as the demo gallery. Mesop also supports creating custom web components based on open web standards, facilitating interoperability with components from different frameworks.

"},{"location":"comparison/#styling-and-customization_1","title":"Styling and Customization","text":"

Gradio features a robust theming system with prebuilt options and extensive UI customization. It also supports custom CSS via direct string construction.

Mesop offers a statically typed Style API for CSS properties. While it includes dark theme support, Mesop's theming capabilities are currently limited and does not allow custom color schemes.

"},{"location":"comparison/#state-management","title":"State management","text":"

Gradio uses an imperative approach to state management, coupling state with component updates. State is typically managed through function parameters and return values, which can be straightforward for simple interfaces but may become complex as applications grow.

Mesop adopts a declarative state management approach, separating state updates from UI rendering. It uses dataclasses for state, providing type-safety and structure for complex states. This separation allows for more granular control over UI updates but may have a steeper learning curve for beginners.

"},{"location":"comparison/#deployment","title":"Deployment","text":"

Gradio makes it easy to share demos via Hugging Face Spaces. Mesop apps can also be deployed on Hugging Face Spaces, but requires a few more steps.

"},{"location":"comparison/#conclusion","title":"Conclusion","text":"

Both Streamlit and Gradio offer gentle learning curves, making it easy for Python developers to quickly build standard AI applications.

Mesop embraces a declarative UI paradigm, which introduces additional concepts but can provide more flexibility for custom applications.

Ultimately, the best choice depends on your specific use case, desired level of customization, and development preferences. We encourage you to explore each framework to determine which best fits your needs.

"},{"location":"demo/","title":"Demo \ud83c\udf10","text":"

hide: - navigation - toc

"},{"location":"faq/","title":"Frequently Asked Questions","text":""},{"location":"faq/#general","title":"General","text":""},{"location":"faq/#what-kinds-of-apps-is-mesop-suited-for","title":"What kinds of apps is Mesop suited for?","text":"

Mesop is well-suited for ML/AI demos and internal tools because it enables developers without frontend experience to quickly build web apps. For use cases that prioritize developer experience and velocity, Mesop can be a good choice.

Demanding consumer-facing apps, which have strict requirements in terms of performance, custom UI components, and i18n/localization would not be a good fit for Mesop and other UI frameworks may be more suitable.

"},{"location":"faq/#how-does-mesop-compare-to-other-python-ui-frameworks","title":"How does Mesop compare to other Python UI frameworks?","text":"

We have written a comparison doc to answer this question in-depth.

"},{"location":"faq/#is-mesop-production-ready","title":"Is Mesop production-ready?","text":"

Dozens of teams at Google have used Mesop to build demos and internal apps.

Although Mesop is pre-v1, we take backwards-compatibilty seriously and avoid backwards incompatible change. This is critical to us because many teams within Google rely on Mesop and we need to not break them.

Occasionally, we will do minor clean-up for our APIs, but we will provide warnings/deprecation notices and provide at least 1 release to migrate to the newer APIs.

"},{"location":"faq/#which-modules-should-i-import-from-mesop","title":"Which modules should I import from Mesop?","text":"

Only import from these two modules:

import mesop as me\nimport mesop.labs as mel\n

All other modules are considered internal implementation details and may change without notice in future releases.

"},{"location":"faq/#is-mesop-an-official-google-product","title":"Is Mesop an official Google product?","text":"

No, Mesop is not an official Google product and Mesop is a 20% project maintained by a small core team of Google engineers with contributions from the broader community.

"},{"location":"faq/#deployment","title":"Deployment","text":""},{"location":"faq/#how-do-i-share-or-deploy-my-mesop-app","title":"How do I share or deploy my Mesop app?","text":"

The best way to share your Mesop app is to deploy it to a cloud service. You can follow our deployment guide for step-by-step instructions to deploy to Google Cloud Run.

Note: you should be able to deploy Mesop on any cloud service that takes a container. Please read the above deployment guide as it should be similar steps.

"},{"location":"goals/","title":"Goals","text":"

I think it's helpful to explicitly state the goals of a project because it provides clarity for not only the development team, but also developers who are evaluating Mesop amongst other options:

  1. Prioritize Python developer experience - Provide the best possible developer experience for Python engineers with minimal frontend experience. Traditional web UI frameworks (e.g. React) prioritize developer experience, but they are focused on web developers who are familiar with the web ecosystem (e.g. HTML, node.js, etc.).
  2. Rich user interactions - You should be able to build reasonably sophisticated web applications and demos (e.g. LLM chat) without building custom native components.
  3. Simple deployment - Make deployment simple by packaging Mesop into a container which can be deployed as a standalone server.
"},{"location":"goals/#examples-of-applying-these-goals","title":"Examples of applying these goals","text":"
  • Web performance: This doesn't mean other goals like web performance have no weight, but we will consistently rank these goals as higher priorities. For example, we could improve performance by serving static assets via CDN, but this would complicate our deployment. For instance, we'd need to ensure that pushing a new Python server binary and JS static assets happened at the same time, or you can get version skews which can cause cryptic errors.

  • Template vs. code: Mesop adopts the pattern of UI-as-code instead of using a separate templating language. Our belief is that writing Python code is a significantly better learning curve for our target developers. Rather than making them learn a new templating language (DSL) that they are unfamiliar with, they can write Python code which allows them idiomatic ways of expressing conditional logic and looping.

"},{"location":"showcase/","title":"Showcase \ud83c\udf10","text":"

hide: - navigation - toc

"},{"location":"api/config/","title":"Config","text":""},{"location":"api/config/#overview","title":"Overview","text":"

Mesop is configured at the application level using environment variables.

"},{"location":"api/config/#configuration-values","title":"Configuration values","text":""},{"location":"api/config/#mesop_static_folder","title":"MESOP_STATIC_FOLDER","text":"

NOTE: By default, this feature is not enabled, but in an upcoming release, the default will be static.

Allows access to static files from the Mesop server.

It is important to know that the specified folder path is relative to the current working directory where the Mesop command is run. Absolute paths are not allowed.

Example:

In this case, the current working directory is /srv, which means Mesop will make /srv/static the static folder.

cd /srv\nMESOP_STATIC_FOLDER=static mesop app/main.py\n

Here are some examples of valid paths. Let's assume the current working directory is /srv/

  • static becomes /srv/static
  • static/ becomes /srv/static
  • static/assets becomes /srv/static/assets
  • ./static becomes /srv/static
  • ./static/ becomes /srv/static
  • ./static/assets becomes /srv/static/assets

Invalid paths will raise MesopDeveloperException. Here are some examples:

  • Absolute paths (e.g. /absolute/path)
  • .
  • ./
  • ..
  • ../
"},{"location":"api/config/#mesop_static_url_path","title":"MESOP_STATIC_URL_PATH","text":"

This is the base URL path from which files for your specified static folder will be made viewable.

The static URL path is only recognized if MESOP_STATIC_FOLDER is set.

For example, given MESOP_STATIC_FOLDER=static and MESOP_STATIC_URL_PATH=/assets, the file static/js/script.js can be viewable from the URL path /assets/js/script.js.

Default: /static

"},{"location":"api/config/#mesop_state_session_backend","title":"MESOP_STATE_SESSION_BACKEND","text":"

Sets the backend to use for caching state data server-side. This makes it so state does not have to be sent to the server on every request, reducing bandwidth, especially if you have large state objects.

The backend options available at the moment are memory, file, sql, and firestore.

"},{"location":"api/config/#memory","title":"memory","text":"

Users should be careful when using the memory backend. Each Mesop process has their own RAM, which means cache misses will be common if each server has multiple processes and there is no session affinity. In addition, the amount of RAM must be carefully specified per instance in accordance with the expected user traffic and state size.

The safest option for using the memory backend is to use a single process with a good amount of RAM. Python is not the most memory efficient, especially when saving data structures such as dicts.

The drawback of being limited to a single process is that requests will take longer to process since only one request can be handled at a time. This is especially problematic if your application contains long running API calls.

If session affinity is available, you can scale up multiple instances, each running single processes.

"},{"location":"api/config/#file","title":"file","text":"

Users should be careful when using the file backend. Each Mesop instance has their own disk, which can be shared among multiple processes. This means cache misses will be common if there are multiple instances and no session affinity.

If session affinity is available, you can scale up multiple instances, each running multiple Mesop processes. If no session affinity is available, then you can only vertically scale a single instance.

The bottleneck with this backend is the disk read/write performance. The amount of disk space must also be carefully specified per instance in accordance with the expected user traffic and state size.

You will also need to specify a directory to write the state data using MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR.

"},{"location":"api/config/#sql","title":"SQL","text":"

NOTE: Setting up and configuring databases is out of scope of this document.

This option uses SqlAlchemy to store Mesop state sessions in supported SQL databases, such as SQLite3 and PostgreSQL. You can also connect to hosted options, such as GCP CloudSQL.

If you use SQLite3, you cannot use an in-memory database. It has to be a file. This option has similar pros/cons as the file backend. Mesop uses the default configuration for SQLite3, so the performance will not be optimized for Mesop's usage patterns. SQLite3 is OK for development purposes.

Using a database like PostgreSQL will allow for better scalability, both vertically and horizontally, since the database is decoupled from the Mesop server.

The drawback here is that this requires knowledge of the database you're using. At minimum, you will need to create a database and a database user with the right privileges. You will also need to create the database table, which you can create with this script. You will need to update the CONNECTION_URI and TABLE_NAME to match your database and settings. Also the database user for this script will need privileges to create tables on the target database.

from sqlalchemy import (\n  Column,\n  DateTime,\n  LargeBinary,\n  MetaData,\n  String,\n  Table,\n  create_engine,\n)\n\nCONNECTION_URI = \"your-database-connection-uri\"\n# Update to \"your-table-name\" if you've overridden `MESOP_STATE_SESSION_BACKEND_SQL_TABLE`.\nTABLE_NAME = \"mesop_state_session\"\n\ndb = create_engine(CONNECTION_URI)\nmetadata = MetaData()\ntable = Table(\n  TABLE_NAME,\n  metadata,\n  Column(\"token\", String(23), primary_key=True),\n  Column(\"states\", LargeBinary, nullable=False),\n  Column(\"created_at\", DateTime, nullable=False, index=True),\n)\n\nmetadata.create_all(db)\n

The Mesop server will raise a sqlalchemy.exc.ProgrammingError if there is a database configuration issue.

By default, Mesop will use the table name mesop_state_session, but this can be overridden using MESOP_STATE_SESSION_BACKEND_SQL_TABLE.

"},{"location":"api/config/#gcp-firestore","title":"GCP Firestore","text":"

This options uses GCP Firestore to store Mesop state sessions. The (default) database has a free tier that can be used for for small demo applications with low traffic and moderate amounts of state data.

Since Firestore is decoupled from your Mesop server, it allows you to scale vertically and horizontally without the considerations you'd need to make for the memory and file backends.

In order to use Firestore, you will need a Google Cloud account with Firestore enabled. Follow the instructions for creating a Firestore in Native mode database.

Mesop is configured to use the (default) Firestore only. The GCP project is determined using the Application Default Credentials (ADC) which is automatically configured for you on GCP services, such as Cloud Run.

For local development, you can run this command:

gcloud auth application-default login\n

If you have multiple GCP projects, you may need to update the project associated with the ADC:

GCP_PROJECT=gcp-project\ngcloud config set project $GCP_PROJECT\ngcloud auth application-default set-quota-project $GCP_PROJECT\n

Mesop leverages Firestore's TTL policies to delete stale state sessions. This needs to be set up using the following command, otherwise old data will accumulate unnecessarily.

COLLECTION_NAME=collection_name\ngcloud firestore fields ttls update expiresAt \\\n  --collection-group=$COLLECTION_NAME\n

By default, Mesop will use the collection name mesop_state_sessions, but this can be overridden using MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION.

Default: none

"},{"location":"api/config/#mesop_state_session_backend_file_base_dir","title":"MESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR","text":"

This is only used when the MESOP_STATE_SESSION_BACKEND is set to file. This parameter specifies where Mesop will read/write the session state. This means the directory must be readable and writeable by the Mesop server processes.

"},{"location":"api/config/#mesop_state_session_backend_firestore_collection","title":"MESOP_STATE_SESSION_BACKEND_FIRESTORE_COLLECTION","text":"

This is only used when the MESOP_STATE_SESSION_BACKEND is set to firestore. This parameter specifies which Firestore collection that Mesop will write state sessions to.

Default: mesop_state_sessions

"},{"location":"api/config/#mesop_state_session_backend_sql_connection_uri","title":"MESOP_STATE_SESSION_BACKEND_SQL_CONNECTION_URI","text":"

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This parameter specifies the database connection string. See the SqlAlchemy docs for more details.

Default: mesop_state_session

"},{"location":"api/config/#mesop_state_session_backend_sql_table","title":"MESOP_STATE_SESSION_BACKEND_SQL_TABLE","text":"

This is only used when the MESOP_STATE_SESSION_BACKEND is set to sql. This parameter specifies which SQL database table that Mesop will write state sessions to.

Default: mesop_state_session

"},{"location":"api/config/#experimental-configuration-values","title":"Experimental configuration values","text":"

These configuration values are experimental and are subject to breaking change, including removal in future releases.

"},{"location":"api/config/#mesop_concurrent_updates_enabled","title":"MESOP_CONCURRENT_UPDATES_ENABLED","text":"

Experimental feature

This is an experimental feature and is subject to breaking change. There are many bugs and edge cases to this feature.

Allows concurrent updates to state in the same session. If this is not updated, then updates are queued and processed sequentially.

By default, this is not enabled. You can enable this by setting it to true.

"},{"location":"api/config/#mesop_web_sockets_enabled","title":"MESOP_WEB_SOCKETS_ENABLED","text":"

Experimental feature

This is an experimental feature and is subject to breaking change. Please follow https://github.com/google/mesop/issues/1028 for updates.

This uses WebSockets instead of HTTP Server-Sent Events (SSE) as the transport protocol for UI updates. If you set this environment variable to true, then MESOP_CONCURRENT_UPDATES_ENABLED will automatically be enabled as well.

"},{"location":"api/config/#usage-examples","title":"Usage Examples","text":""},{"location":"api/config/#one-liner","title":"One-liner","text":"

You can specify the environment variables before the mesop command.

MESOP_STATE_SESSION_BACKEND=memory mesop main.py\n
"},{"location":"api/config/#use-a-env-file","title":"Use a .env file","text":"

Mesop also supports .env files. This is nice since you don't have to keep setting the environment variables. In addition, the variables are only set when the application is run.

.env
MESOP_STATE_SESSION_BACKEND=file\nMESOP_STATE_SESSION_BACKEND_FILE_BASE_DIR=/tmp/mesop-sessions\n

When you run your Mesop app, the .env file will then be read.

mesop main.py\n
"},{"location":"api/page/","title":"Page API","text":""},{"location":"api/page/#overview","title":"Overview","text":"

Pages allow you to build multi-page applications by decorating Python functions with me.page. To learn more, read the see multi-pages guide.

"},{"location":"api/page/#examples","title":"Examples","text":""},{"location":"api/page/#simple-1-page-setup","title":"Simple, 1-page setup","text":"

To create a simple Mesop app, you can use me.page() like this:

import mesop as me\n\n@me.page()\ndef foo():\n    me.text(\"bar\")\n

NOTE: If you do not provide a path argument, then it defaults to the root path \"/\".

"},{"location":"api/page/#explicit-1-page-setup","title":"Explicit 1-page setup","text":"

This is the same as the above example which explicitly sets the route to \"/\".

import mesop as me\n\n@me.page(path=\"/\")\ndef foo():\n    me.text(\"bar\")\n
"},{"location":"api/page/#api","title":"API","text":""},{"location":"api/page/#mesop.features.page.page","title":"page","text":"

Defines a page in a Mesop application.

This function is used as a decorator to register a function as a page in a Mesop app.

PARAMETER DESCRIPTION path

The URL path for the page. Defaults to \"/\".

TYPE: str DEFAULT: '/'

title

The title of the page. If None, a default title is generated.

TYPE: str | None DEFAULT: None

stylesheets

List of stylesheet URLs to load.

TYPE: list[str] | None DEFAULT: None

security_policy

The security policy for the page. If None, a default strict security policy is used.

TYPE: SecurityPolicy | None DEFAULT: None

on_load

An optional event handler to be called when the page is loaded.

TYPE: OnLoadHandler | None DEFAULT: None

RETURNS DESCRIPTION Callable[[Callable[[], None]], Callable[[], None]]

A decorator that registers the decorated function as a page.

"},{"location":"api/page/#mesop.security.security_policy.SecurityPolicy","title":"SecurityPolicy dataclass","text":"

A class to represent the security policy.

ATTRIBUTE DESCRIPTION allowed_iframe_parents

A list of allowed iframe parents.

TYPE: list[str]

allowed_connect_srcs

A list of sites you can connect to, see MDN.

TYPE: list[str]

allowed_script_srcs

A list of sites you load scripts from, see MDN.

TYPE: list[str]

dangerously_disable_trusted_types

A flag to disable trusted types. Highly recommended to not disable trusted types because it's an important web security feature!

TYPE: bool

"},{"location":"api/page/#mesop.events.events.LoadEvent","title":"LoadEvent dataclass","text":"

Represents a page load event.

ATTRIBUTE DESCRIPTION path

The path loaded

TYPE: str

"},{"location":"api/page/#on_load","title":"on_load","text":"

You may want to do some sort of data-processing when a page is first loaded in a session.

"},{"location":"api/page/#simple-handler","title":"Simple handler","text":"

An on_load handler is similar to a regular event handler where you can mutate state.

import time\n\nimport mesop as me\n\n\ndef fake_api():\n  yield 1\n  time.sleep(1)\n  yield 2\n  time.sleep(2)\n  yield 3\n\n\ndef on_load(e: me.LoadEvent):\n  for val in fake_api():\n    me.state(State).default_values.append(val)\n    yield\n\n\n@me.page(path=\"/docs/on_load\", on_load=on_load)\ndef app():\n  me.text(\"onload\")\n  me.text(str(me.state(State).default_values))\n\n\n@me.stateclass\nclass State:\n  default_values: list[int]\n
"},{"location":"api/page/#generator-handler","title":"Generator handler","text":"

The on_load handler can also be a generator function. This is useful if you need to call a slow or streaming API and want to return intermediate results before all the data has been received.

import time\n\nimport mesop as me\n\n\ndef on_load(e: me.LoadEvent):\n  state = me.state(State)\n  state.default_values.append(\"a\")\n  yield\n  time.sleep(1)\n  state.default_values.append(\"b\")\n  yield\n\n\n@me.page(path=\"/docs/on_load_generator\", on_load=on_load)\ndef app():\n  me.text(\"onload\")\n  me.text(str(me.state(State).default_values))\n\n\n@me.stateclass\nclass State:\n  default_values: list[str]\n
"},{"location":"api/query-params/","title":"Query Params API","text":""},{"location":"api/query-params/#overview","title":"Overview","text":"

Query params, also sometimes called query string, provide a way to manage state in the URLs. They are useful for providing deep-links into your Mesop app.

"},{"location":"api/query-params/#example","title":"Example","text":"

Here's a simple working example that shows how you can read and write query params.

@me.page(path=\"/examples/query_params/page_2\")\ndef page_2():\n  me.text(f\"query_params={me.query_params}\")\n  me.button(\"Add query param\", on_click=add_query_param)\n  me.button(\"Navigate\", on_click=navigate)\n\ndef add_query_param(e: me.ClickEvent):\n  me.query_params[\"key\"] = \"value\"\n\ndef navigate(e: me.ClickEvent):\n  me.navigate(\"/examples/query_params\", query_params=me.query_params)\n
"},{"location":"api/query-params/#usage","title":"Usage","text":"

You can use query parameters from me.query_params, which has a dictionary-like interface, where the key is the parameter name and value is the parameter value.

"},{"location":"api/query-params/#get-a-query-param-value","title":"Get a query param value","text":"

value: str = me.query_params['param_name']\n
This will raise a KeyError if the parameter doesn't exist. You can use in to check whether a key exists in me.query_params:

if 'key' in me.query_params:\n    print(me.query_params['key'])\n
Repeated query params

If a query param key is repeated, then you will get the first value. If you want all the values use get_all.

"},{"location":"api/query-params/#get-all-values","title":"Get all values","text":"

To get all the values for a particular query parameter key, you can use me.query_params.get_all, which returns a sequence of parameter values (currently implemented as a tuple).

all_values = me.query_params.get_all('param_name')\n
"},{"location":"api/query-params/#iterate","title":"Iterate","text":"
for key in query_params:\n  value = query_params[key]\n
"},{"location":"api/query-params/#set-query-param","title":"Set query param","text":"
query_params['new_param'] = 'value'\n
"},{"location":"api/query-params/#set-repeated-query-param","title":"Set repeated query param","text":"
query_params['repeated_param'] = ['value1', 'value2']\n
"},{"location":"api/query-params/#delete","title":"Delete","text":"
del query_params['param_to_delete']\n
"},{"location":"api/query-params/#patterns","title":"Patterns","text":""},{"location":"api/query-params/#navigate-with-existing-query-params","title":"Navigate with existing query params","text":"

Here's an example of how to navigate to a new page with query parameters:

def click_navigate_button(e: me.ClickEvent):\n    me.query_params['q'] = \"value\"\n    me.navigate('/search', query_params=me.query_params)\n
"},{"location":"api/query-params/#navigate-with-only-new-query-params","title":"Navigate with only new query params","text":"

You can also navigate by passing in a dictionary to query_params parameter for me.navigate if you do not want to keep the existing query parameters.

def click_navigate_button(e: me.ClickEvent):\n    me.navigate('/search', query_params={\"q\": \"value})\n
"},{"location":"api/style/","title":"Style","text":""},{"location":"api/style/#overview","title":"Overview","text":"

Mesop provides a Python API that wraps the browser's native CSS style API.

"},{"location":"api/style/#api","title":"API","text":""},{"location":"api/style/#mesop.component_helpers.style.Style","title":"Style dataclass","text":"

Represents the style configuration for a UI component.

ATTRIBUTE DESCRIPTION align_content

Aligns the flexible container's items on the cross-axis. See MDN doc.

TYPE: ContentAlignmentValues | None

align_items

Specifies the default alignment for items inside a flexible container. See MDN doc.

TYPE: ItemAlignmentValues | None

align_self

Overrides a grid or flex item's align-items value. In Grid, it aligns the item inside the grid area. In Flexbox, it aligns the item on the cross axis. See MDN doc.

TYPE: ItemAlignmentValues | None

aspect_ratio

Specifies the desired width-to-height ratio of a component. See MDN doc.

TYPE: str | None

backdrop_filter

Applies a CSS filter to the backdrop of the component. See MDN doc.

TYPE: str | None

background

Sets the background color or image of the component. See MDN doc.

TYPE: str | None

border

Defines the border properties for each side of the component. See MDN doc.

TYPE: Border | None

border_radius

Defines the border radius. See MDN doc.

TYPE: int | str | None

bottom

Helps set vertical position of a positioned element. See MDN doc.

TYPE: int | str | None

box_shadow

Defines the box shadow. See MDN doc.

TYPE: str | None

box_sizing

Defines the box sizing. See MDN doc.

TYPE: str | None

color

Sets the color of the text inside the component. See MDN doc.

TYPE: str | None

column_gap

Sets the gap between columns. See MDN doc.

TYPE: int | str | None

columns

Specifies the number of columns in a multi-column element. See MDN doc.

TYPE: int | str | None

cursor

Sets the mouse cursor. See MDN doc.

TYPE: str | None

display

Defines the display type of the component. See MDN doc.

TYPE: Literal['block', 'inline', 'inline-block', 'flex', 'inline-flex', 'grid', 'inline-grid', 'none', 'contents'] | None

flex

Defines the flexbox layout using a shorthand property. See MDN doc.

TYPE: int | str | None

flex_basis

Specifies the initial length of a flexible item. See MDN doc.

TYPE: str | None

flex_direction

Establishes the main-axis, thus defining the direction flex items are placed in the flex container. See MDN doc.

TYPE: Literal['row', 'row-reverse', 'column', 'column-reverse'] | None

flex_grow

Defines the ability for a flex item to grow if necessary. See MDN doc.

TYPE: int | None

flex_shrink

Defines the ability for a flex item to shrink if necessary. See MDN doc.

TYPE: int | None

flex_wrap

Allows flex items to wrap onto multiple lines. See MDN doc.

TYPE: Literal['nowrap', 'wrap', 'wrap-reverse'] | None

font_family

Specifies the font family. See MDN doc.

TYPE: str | None

font_size

Sets the size of the font. See MDN doc.

TYPE: int | str | None

font_style

Specifies the font style for text. See MDN doc.

TYPE: Literal['italic', 'normal'] | None

font_weight

Sets the weight (or boldness) of the font. See MDN doc.

TYPE: Literal['bold', 'normal', 'medium', 100, 200, 300, 400, 500, 600, 700, 800, 900] | None

gap

Sets the gap. See MDN doc.

TYPE: int | str | None

grid_area

Sets the grid area. See MDN doc.

TYPE: str | None

grid_auto_columns

CSS property specifies the size of an implicitly-created grid column track or pattern of tracks. See MDN doc.

TYPE: str | None

grid_auto_flow

CSS property controls how the auto-placement algorithm works, specifying exactly how auto-placed items get flowed into the grid. See MDN doc.

TYPE: str | None

grid_auto_rows

CSS property specifies the size of an implicitly-created grid row track or pattern of tracks. See MDN doc.

TYPE: str | None

grid_column

CSS shorthand property specifies a grid item's size and location within a grid column. See MDN doc.

TYPE: str | None

grid_column_start

Sets the grid column start. See MDN doc.

TYPE: int | str | None

grid_column_end

Sets the grid column end. See MDN doc.

TYPE: int | str | None

grid_row

CSS shorthand property specifies a grid item's size and location within a grid row. See MDN doc.

TYPE: str | None

grid_row_start

Sets the grid row start. See MDN doc.

TYPE: int | str | None

grid_row_end

Sets the grid row end. See MDN doc.

TYPE: int | str | None

grid_template_areas

Sets the grid template areas; each element is a row. See MDN doc.

TYPE: list[str] | None

grid_template_columns

Sets the grid template columns. See MDN doc.

TYPE: str | None

grid_template_rows

Sets the grid template rows. See MDN doc.

TYPE: str | None

height

Sets the height of the component. See MDN doc.

TYPE: int | str | None

justify_content

Aligns the flexible container's items on the main-axis. See MDN doc.

TYPE: ContentAlignmentValues | None

justify_items

Defines the default justify-self for all items of the box, giving them all a default way of justifying each box along the appropriate axis. See MDN doc.

TYPE: ItemJustifyValues | None

justify_self

Sets the way a box is justified inside its alignment container along the appropriate axis. See MDN doc.

TYPE: ItemJustifyValues | None

left

Helps set horizontal position of a positioned element. See MDN doc.

TYPE: int | str | None

letter_spacing

Increases or decreases the space between characters in text. See MDN doc.

TYPE: int | str | None

line

Set the line height (relative to the font size). See MDN doc.

TYPE: height

margin

Sets the margin space required on each side of an element. See MDN doc.

TYPE: Margin | None

max_height

Sets the maximum height of an element. See MDN doc.

TYPE: int | str | None

max_width

Sets the maximum width of an element. See MDN doc.

TYPE: int | str | None

min_height

Sets the minimum height of an element. See MDN doc.

TYPE: int | str | None

min_width

Sets the minimum width of an element. See MDN doc.

TYPE: int | str | None

object_fit

Specifies how an image or video should be resized to fit its container. See MDN doc.

TYPE: ObjectFitValues | None

opacity

Sets the opacity property. See MDN doc.

TYPE: float | str | None

outline

Sets the outline property. Note: input component has default browser stylings. See MDN doc.

TYPE: str | None

overflow_wrap

Specifies how long text can be broken up by new lines to prevent overflowing. See MDN doc.

TYPE: OverflowWrapValues | None

overflow

Specifies the handling of overflow in the horizontal and vertical direction. See MDN doc.

TYPE: OverflowValues | None

overflow_x

Specifies the handling of overflow in the horizontal direction. See MDN doc.

TYPE: OverflowValues | None

overflow_y

Specifies the handling of overflow in the vertical direction. See MDN doc.

TYPE: OverflowValues | None

padding

Sets the padding space required on each side of an element. See MDN doc.

TYPE: Padding | None

place_items

The CSS place-items shorthand property allows you to align items along both the block and inline directions at once. See MDN doc.

TYPE: str | None

pointer_events

Sets under what circumstances (if any) a particular graphic element can become the target of pointer events. See MDN doc.

TYPE: PointerEventsValues | None

position

Specifies the type of positioning method used for an element (static, relative, absolute, fixed, or sticky). See MDN doc.

TYPE: Literal['static', 'relative', 'absolute', 'fixed', 'sticky'] | None

right

Helps set horizontal position of a positioned element. See MDN doc.

TYPE: int | str | None

rotate

Allows you to specify rotation transforms individually and independently of the transform property. See MDN doc.

TYPE: str | None

row_gap

Sets the gap between rows. See MDN doc.

TYPE: int | str | None

text_align

Specifies the horizontal alignment of text in an element. See MDN doc.

TYPE: Literal['start', 'end', 'left', 'right', 'center'] | None

text_decoration

Specifies the decoration added to text. See MDN doc.

TYPE: Literal['underline', 'none'] | None

text_overflow

Specifies how overflowed content that is not displayed should be signaled to the user. See MDN doc.

TYPE: Literal['ellipsis', 'clip'] | None

text_shadow

Specifies the shadow effect applied to text. See MDN doc.

TYPE: str | None

text_transform

Specifies the transformation applied to text. See MDN doc.

TYPE: Literal['uppercase', 'lowercase', 'capitalize', 'none', 'full-width', 'full-size-kana'] | None

top

Helps set vertical position of a positioned element. See MDN doc.

TYPE: int | str | None

transform

Lets you rotate, scale, skew, or translate an element. It modifies the coordinate space of the CSS visual formatting model. See MDN doc.

TYPE: str | None

transition

Specifies the transition effect. See MDN doc.

TYPE: str | None

vertical_align

Specifies the vertical alignment of an element. See MDN doc.

TYPE: Literal['baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'initial', 'inherit', 'revert', 'revert-layer', 'unset'] | None

visibility

Sets the visibility property. See MDN doc.

TYPE: Literal['visible', 'hidden', 'collapse', 'inherit', 'initial', 'revert', 'revert-layer', 'unset'] | None

white_space

Specifies how white space inside an element is handled. See MDN doc.

TYPE: Literal['normal', 'nowrap', 'pre', 'pre-wrap', 'pre-line', 'break-spaces'] | None

width

Sets the width of the component. See MDN doc.

TYPE: int | str | None

word_wrap

Specifies how long text can be broken up by new lines to prevent overflowing. See MDN doc.

TYPE: Literal['normal', 'break-word', 'anywhere'] | None

z-index

Sets the z-index of the component. See MDN doc.

TYPE: Literal['normal', 'break-word', 'anywhere'] | None

"},{"location":"api/style/#mesop.component_helpers.style.Border","title":"Border dataclass","text":"

Defines the border styles for each side of a UI component.

ATTRIBUTE DESCRIPTION top

Style for the top border.

TYPE: BorderSide | None

right

Style for the right border.

TYPE: BorderSide | None

bottom

Style for the bottom border.

TYPE: BorderSide | None

left

Style for the left border.

TYPE: BorderSide | None

"},{"location":"api/style/#mesop.component_helpers.style.Border.all","title":"all staticmethod","text":"

Creates a Border instance with all sides having the same style.

PARAMETER DESCRIPTION value

The style to apply to all sides of the border.

TYPE: BorderSide

RETURNS DESCRIPTION Border

A new Border instance with the specified style applied to all sides.

TYPE: Border

"},{"location":"api/style/#mesop.component_helpers.style.Border.symmetric","title":"symmetric staticmethod","text":"

Creates a Border instance with symmetric styles for vertical and horizontal sides.

PARAMETER DESCRIPTION vertical

The style to apply to the top and bottom sides of the border.

TYPE: BorderSide | None DEFAULT: None

horizontal

The style to apply to the right and left sides of the border.

TYPE: BorderSide | None DEFAULT: None

RETURNS DESCRIPTION Border

A new Border instance with the specified styles applied symmetrically.

TYPE: Border

"},{"location":"api/style/#mesop.component_helpers.style.BorderSide","title":"BorderSide dataclass","text":"

Represents the style of a single side of a border in a UI component.

ATTRIBUTE DESCRIPTION width

The width of the border. Can be specified as an integer value representing pixels, a string with a unit (e.g., '2em'), or None for no width.

TYPE: int | str | None

color

The color of the border, represented as a string. This can be any valid CSS color value, or None for no color.

TYPE: str | None

style

The style of the border. See https://developer.mozilla.org/en-US/docs/Web/CSS/border-style

TYPE: Literal['none', 'solid', 'dashed', 'dotted', 'double', 'groove', 'ridge', 'inset', 'outset', 'hidden'] | None

"},{"location":"api/style/#mesop.component_helpers.style.Margin","title":"Margin dataclass","text":"

Bases: _EdgeInsets

Defines the margin space around a UI component.

ATTRIBUTE DESCRIPTION top

Top margin (note: 2 is the same as 2px)

TYPE: int | str | None

right

Right margin

TYPE: int | str | None

bottom

Bottom margin

TYPE: int | str | None

left

Left margin

TYPE: int | str | None

"},{"location":"api/style/#mesop.component_helpers.style.Margin.all","title":"all staticmethod","text":"

Creates a Margin instance with the same value for all sides.

PARAMETER DESCRIPTION value

The value to apply to all sides of the margin. Can be an integer (pixel value) or a string.

TYPE: int | str

RETURNS DESCRIPTION Margin

A new Margin instance with the specified value applied to all sides.

TYPE: Margin

"},{"location":"api/style/#mesop.component_helpers.style.Margin.symmetric","title":"symmetric staticmethod","text":"

Creates a Margin instance with symmetric values for vertical and horizontal sides.

PARAMETER DESCRIPTION vertical

The value to apply to the top and bottom sides of the margin. Can be an integer (pixel value) or a string.

TYPE: int | str | None DEFAULT: None

horizontal

The value to apply to the right and left sides of the margin. Can be an integer (pixel value) or a string.

TYPE: int | str | None DEFAULT: None

RETURNS DESCRIPTION Margin

A new Margin instance with the specified values applied to the vertical and horizontal sides.

TYPE: Margin

"},{"location":"api/style/#mesop.component_helpers.style.Padding","title":"Padding dataclass","text":"

Bases: _EdgeInsets

Defines the padding space around a UI component.

ATTRIBUTE DESCRIPTION top

Top padding (note: 2 is the same as 2px)

TYPE: int | str | None

right

Right padding

TYPE: int | str | None

bottom

Bottom padding

TYPE: int | str | None

left

Left padding

TYPE: int | str | None

"},{"location":"api/style/#mesop.component_helpers.style.Padding.all","title":"all staticmethod","text":"

Creates a Padding instance with the same value for all sides.

PARAMETER DESCRIPTION value

The value to apply to all sides of the padding. Can be an integer (pixel value) or a string.

TYPE: int | str

RETURNS DESCRIPTION Padding

A new Padding instance with the specified value applied to all sides.

TYPE: Padding

"},{"location":"api/style/#mesop.component_helpers.style.Padding.symmetric","title":"symmetric staticmethod","text":"

Creates a Padding instance with symmetric values for vertical and horizontal sides.

PARAMETER DESCRIPTION vertical

The value to apply to the top and bottom sides of the padding. Can be an integer (pixel value) or a string.

TYPE: int | str | None DEFAULT: None

horizontal

The value to apply to the right and left sides of the padding. Can be an integer (pixel value) or a string.

TYPE: int | str | None DEFAULT: None

RETURNS DESCRIPTION Padding

A new Padding instance with the specified values applied to the vertical and horizontal sides.

TYPE: Padding

"},{"location":"api/viewport-size/","title":"Viewport size","text":""},{"location":"api/viewport-size/#overview","title":"Overview","text":"

The viewport size API allows you to access the current viewport size. This can be useful for creating responsive and adaptive designs that are suitable for the user's screen size.

"},{"location":"api/viewport-size/#examples","title":"Examples","text":""},{"location":"api/viewport-size/#responsive-design","title":"Responsive Design","text":"

Responsive design is having a single fluid layout that adapts to all screen sizes.

You can use the viewport size to dynamically set the property of a style. This can be useful if you want to fit two boxes in a row for larger screens (e.g. desktop) and a single box for smaller screens (e.g. mobile) as shown in the example below:

import mesop as me\n\n@me.page()\ndef page():\n    if me.viewport_size().width > 640:\n        width = me.viewport_size().width / 2\n    else:\n        width = me.viewport_size().width\n    for i in range(8):\n      me.box(style=me.Style(width=width))\n

Tip: Responsive design tends to take less work and is usually a good starting point.

"},{"location":"api/viewport-size/#adaptive-design","title":"Adaptive Design","text":"

Adaptive design is having multiple fixed layouts for specific device categories at specific breakpoints, typically viewport width.

For example, oftentimes you will hide the nav component on a mobile device and instead show a hamburger menu, while for a larger device you will always show the nav component on the left side.

import mesop as me\n\n@me.page()\ndef page():\n    if me.viewport_size().width > 480:\n        nav_component()\n        body()\n    else:\n        body(show_menu_button=True)\n

Tip: Adaptive design tends to take more work and is best for optimizing complex mobile and desktop experiences.

"},{"location":"api/viewport-size/#api","title":"API","text":""},{"location":"api/viewport-size/#mesop.features.viewport_size.viewport_size","title":"viewport_size","text":"

Returns the current viewport size.

RETURNS DESCRIPTION Size

The current viewport size.

TYPE: Size

"},{"location":"api/viewport-size/#mesop.features.viewport_size.Size","title":"Size dataclass","text":"ATTRIBUTE DESCRIPTION width

The width of the viewport in pixels.

TYPE: int

height

The height of the viewport in pixels.

TYPE: int

"},{"location":"api/commands/focus-component/","title":"Focus component","text":"

If you want to focus on a component, you can use me.focus_component which focuses the component with the specified key if it is focusable.

"},{"location":"api/commands/focus-component/#example","title":"Example","text":"
import mesop as me\n\n\n@me.page(path=\"/focus_component\")\ndef page():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.select(\n      options=[\n        me.SelectOption(label=\"Autocomplete\", value=\"autocomplete\"),\n        me.SelectOption(label=\"Checkbox\", value=\"checkbox\"),\n        me.SelectOption(label=\"Input\", value=\"input\"),\n        me.SelectOption(label=\"Link\", value=\"link\"),\n        me.SelectOption(label=\"Radio\", value=\"radio\"),\n        me.SelectOption(label=\"Select\", value=\"select\"),\n        me.SelectOption(label=\"Slider\", value=\"slider\"),\n        me.SelectOption(label=\"Slide Toggle\", value=\"slide_toggle\"),\n        me.SelectOption(label=\"Textarea\", value=\"textarea\"),\n        me.SelectOption(label=\"Uploader\", value=\"uploader\"),\n      ],\n      on_selection_change=on_selection_change,\n    )\n\n  me.divider()\n\n  with me.box(\n    style=me.Style(\n      display=\"grid\",\n      gap=5,\n      grid_template_columns=\"1fr 1fr\",\n      margin=me.Margin.all(15),\n    )\n  ):\n    with me.box():\n      me.autocomplete(\n        key=\"autocomplete\",\n        label=\"Autocomplete\",\n        options=[\n          me.AutocompleteOption(label=\"Test\", value=\"Test\"),\n          me.AutocompleteOption(label=\"Test2\", value=\"Tes2t\"),\n        ],\n      )\n\n    with me.box():\n      me.checkbox(\"Checkbox\", key=\"checkbox\")\n\n    with me.box():\n      me.input(key=\"input\", label=\"Input\")\n\n    with me.box():\n      me.link(key=\"link\", text=\"Test\", url=\"https://google.com\")\n\n    with me.box():\n      me.radio(\n        key=\"radio\",\n        options=[\n          me.RadioOption(label=\"Option 1\", value=\"1\"),\n          me.RadioOption(label=\"Option 2\", value=\"2\"),\n        ],\n      )\n\n    with me.box():\n      me.select(\n        key=\"select\",\n        label=\"Select\",\n        options=[\n          me.SelectOption(label=\"label 1\", value=\"value1\"),\n          me.SelectOption(label=\"label 2\", value=\"value2\"),\n          me.SelectOption(label=\"label 3\", value=\"value3\"),\n        ],\n      )\n\n    with me.box():\n      me.slider(key=\"slider\")\n\n    with me.box():\n      me.slide_toggle(key=\"slide_toggle\", label=\"Slide toggle\")\n\n    with me.box():\n      me.textarea(key=\"textarea\", label=\"Textarea\")\n\n    with me.box():\n      me.uploader(\n        key=\"uploader\",\n        label=\"Upload Image\",\n        accepted_file_types=[\"image/jpeg\", \"image/png\"],\n        type=\"flat\",\n        color=\"primary\",\n        style=me.Style(font_weight=\"bold\"),\n      )\n\n\ndef on_selection_change(e: me.SelectSelectionChangeEvent):\n  me.focus_component(key=e.value)\n
"},{"location":"api/commands/focus-component/#api","title":"API","text":""},{"location":"api/commands/focus-component/#mesop.commands.focus_component.focus_component","title":"focus_component","text":"

Focus the component specified by the key

PARAMETER DESCRIPTION key

The unique identifier of the component to focus on. This key should be globally unique to prevent unexpected behavior. If multiple components share the same key, the first component instance found in the component tree will be focused on.

TYPE: str

"},{"location":"api/commands/navigate/","title":"Navigate","text":"

To navigate to another page, you can use me.navigate. This is particularly useful for navigating across a multi-page app.

"},{"location":"api/commands/navigate/#example","title":"Example","text":"
import mesop as me\n\n\ndef navigate(event: me.ClickEvent):\n  me.navigate(\"/about\")\n\n\n@me.page(path=\"/\")\ndef home():\n  me.text(\"This is the home page\")\n  me.button(\"navigate to about page\", on_click=navigate)\n\n\n@me.page(path=\"/about\")\ndef about():\n  me.text(\"This is the about page\")\n
"},{"location":"api/commands/navigate/#api","title":"API","text":""},{"location":"api/commands/navigate/#mesop.commands.navigate.navigate","title":"navigate","text":"

Navigates to the given URL.

PARAMETER DESCRIPTION url

The URL to navigate to.

TYPE: str

query_params

A dictionary of query parameters to include in the URL, or me.query_params. If not provided, all current query parameters will be removed.

TYPE: dict[str, str | Sequence[str]] | QueryParams | None DEFAULT: None

"},{"location":"api/commands/scroll-into-view/","title":"Scroll into view","text":"

If you want to scroll a component into the viewport, you can use me.scroll_into_view which scrolls the component with the specified key into the viewport.

"},{"location":"api/commands/scroll-into-view/#example","title":"Example","text":"
import time\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  more_lines: int = 0\n\n\n@me.page(path=\"/scroll_into_view\")\ndef app():\n  me.button(\"Scroll to middle line\", on_click=scroll_to_middle)\n  me.button(\"Scroll to bottom line\", on_click=scroll_to_bottom)\n  me.button(\n    \"Scroll to bottom line & generate lines\",\n    on_click=scroll_to_bottom_and_generate_lines,\n  )\n  for _ in range(100):\n    me.text(\"Filler line\")\n  me.text(\"middle_line\", key=\"middle_line\")\n  for _ in range(100):\n    me.text(\"Filler line\")\n  me.text(\"bottom_line\", key=\"bottom_line\")\n  for _ in range(me.state(State).more_lines):\n    me.text(\"More lines\")\n\n\ndef scroll_to_middle(e: me.ClickEvent):\n  me.scroll_into_view(key=\"middle_line\")\n\n\ndef scroll_to_bottom(e: me.ClickEvent):\n  me.scroll_into_view(key=\"bottom_line\")\n\n\ndef scroll_to_bottom_and_generate_lines(e: me.ClickEvent):\n  state = me.state(State)\n  me.scroll_into_view(key=\"bottom_line\")\n  yield\n  state.more_lines += 5\n  time.sleep(1)\n  yield\n  state.more_lines += 5\n  time.sleep(1)\n  yield\n  state.more_lines += 5\n  time.sleep(1)\n  yield\n  state.more_lines += 5\n  time.sleep(1)\n  yield\n
"},{"location":"api/commands/scroll-into-view/#api","title":"API","text":""},{"location":"api/commands/scroll-into-view/#mesop.commands.scroll_into_view.scroll_into_view","title":"scroll_into_view","text":"

Scrolls so the component specified by the key is in the viewport.

PARAMETER DESCRIPTION key

The unique identifier of the component to scroll to. This key should be globally unique to prevent unexpected behavior. If multiple components share the same key, the first component instance found in the component tree will be scrolled to.

TYPE: str

"},{"location":"api/commands/set-page-title/","title":"Set page title","text":"

If you want to set the page title, you can use me.set_page_title which will set the page title displayed on the browser tab.

This change does not persist if you navigate to a new page. The title will be reset to the title configured in me.page.

"},{"location":"api/commands/set-page-title/#example","title":"Example","text":"
import mesop as me\n\n\ndef on_blur(e: me.InputBlurEvent):\n  me.set_page_title(e.value)\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/set_page_title\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.input(label=\"Page title\", on_blur=on_blur)\n
"},{"location":"api/commands/set-page-title/#api","title":"API","text":""},{"location":"api/commands/set-page-title/#mesop.commands.set_page_title.set_page_title","title":"set_page_title","text":"

Sets the page title.

PARAMETER DESCRIPTION title

The new page title

TYPE: str

"},{"location":"blog/","title":"Blog Home","text":""},{"location":"blog/2023/12/25/hello-mesop/","title":"Hello, Mesop","text":"

After working on Mesop for the last two months, I'm excited to finally announce the first version of Mesop, v0.1. This is still early days for Mesop, but it's an important milestone because it represents a minimum viable tool for building UIs in Python. In case you haven't read Mesop's home page, Mesop is a Python-based UI framework that allows you to rapidly build web demos. Engineers without frontend experience can build web UIs by writing idiomatic Python code.

"},{"location":"blog/2023/12/25/hello-mesop/#why-mesop","title":"Why Mesop?","text":"

Mesop is in many ways a remix of many existing ideas packaged into a single cohesive UI framework, designed for Python developers. I've documented some of these goals previously, but I'll quickly recap the benefits of Mesop here:

  • Allows non-frontend engineers to rapidly build UIs for internal use cases like demos.
  • Provides a fast build-edit-refresh loop through hot reload.
  • Enables developers to benefit from the mature Angular web framework and Angular Material components.
  • Provides a flexible and composable components API that's idiomatic to Python.
  • Easy to deploy by using standard HTTP technologies like Server-Sent Events.
"},{"location":"blog/2023/12/25/hello-mesop/#whats-next-for-mesop","title":"What's next for Mesop?","text":"

I see a few broad themes of work in the coming year or so.

"},{"location":"blog/2023/12/25/hello-mesop/#expand-mesops-component-library","title":"Expand Mesop's component library","text":"

Mesop's current component library is a solid start but there's still gaps to support common use cases.

Areas of work:

  • Complete Angular Material component coverage. We support 13+ Angular Material components today, however there's many more that we don't support. Some of it is because I haven't had time to wrap their components, but in other cases (e.g. sidenav), I'd like to spend more time exploring the design space as it will probably require supporting some kind of multi-slot component API. Getting this API designed correctly, for not just this component but also future components, is important in the long run.

  • Support more native HTML elements/browser APIs. Right now, only Box and Text are thin wrappers around native HTML elements. However, there are other HTML elements like <img>, <audio> and <video> that I'd like to also support. The flip side of supporting these components is enabling a way to allow Mesop end-users to upload these media contents, which there are also native browser APIs for.

  • Custom components. Some components won't belong in the standard Mesop package because it's either too experimental or too use-case specific. It would be nice to have a complete story for supporting custom components. Today, all of the components use the component helper API which wraps internal framework details like runtime. However, there still isn't a very good story for loading custom components in the Angular frontend (e.g. ComponentRenderer's type to component map) and testing them.

"},{"location":"blog/2023/12/25/hello-mesop/#make-it-easy-to-get-started-with-mesop","title":"Make it easy to get started with Mesop","text":"

Using Mesop today requires following our internal development setup which requires dependencies like Bazel/iBazel which makes it easy to interoperate with our downstream sync, but these dependencies aren't commonly used in the Python ecosystem. Eventually, I'd like make using Mesop as simple as pip install mesop and then using Mesop's built-in CLI: mesop serve for local development and mesop deploy to deploy on a Cloud service.

Areas of work:

  • Find a suitable ibazel replacement for Hot Reload. Instead of requiring Mesop developers to sync the entire repo and building the project with Bazel and iBazel, we should distribute a ready-to-use pip package of Mesop. However, this leaves an open question of how we support hot reload without iBazel which provides: 1) a filesystem watching mechanism and 2) live reload. We'll need to investigate good open-source equivalents for each of these capabilities.

  • Provide web-based interactive demos. Many JavaScript UI frameworks provide a playground (e.g. Angular) or interactive tutorial (e.g. Solid) so that prospective developers can use the framework before going through the hassle of setting up their own local dev environment. This would also be very helpful to provide for each component as it's a lot easier to understand a component by tinkering with a live example.

"},{"location":"blog/2023/12/25/hello-mesop/#explore-power-use-cases","title":"Explore power use cases","text":"

Today Mesop is good for internal apps with relatively un-stringent demands in terms of UI customizability and performance. For production-grade external apps, there's several areas that Mesop would need to advance in, before it's ready.

Areas of work:

  • Optimize network payload. Right now the client sends the entire state to the server, and the server responds with the entire state and component tree. For large UIs/apps, this can result in sizable network payloads. We can optimize this by sending deltas as much as possible. For example, the server can send a delta of the state and component tree to the client. In addition, if we use POST instead of GET, we can stop using base-64 encoding which adds a significant overhead on top of Protobuf binary serialization.

  • Stateful server. Even with the above optimizations, we'd essentially preserve the current architecture, but there's some limitations in how much improvements we can make as long as we assume servers are stateless. However, if we allow stateful servers (i.e. long-lived connections between the client and server), we can use things like WebSockets and always send deltas bi-directionally, in particular from client to server which isn't possible with a stateless server. The problem with this direction, though, is that it makes deployment more complex as scaling a WebSocket-based server can be hard depending on the cloud infrastructure used. In addition, we'll need to handle new edge cases like authentication and broken WebSockets connections.

  • Optimistic UI. One of the drawbacks for server-driven UI frameworks like Mesop is that it introduces significant latency to simple user interactions. For example, if you click a button, it requires a network roundtrip before the UI is (meaningfully) updated. One way of dealing with this shortcoming is by pre-fetching the next UI state based on a user hint. For example, if a user is hovering over a button, we could optimistically calculate the state change and component tree change ahead of time before the actual click. The obvious downside to this is that optimistically executing an action is inappropriate in many cases, for example, a non-reversible action (e.g. delete) should never be optimistically done. To safely introduce this concept, we could provide an (optional) annotation for event handlers like @me.optimistic(events=[me.HoverEvent]) so develpers could opt-in.

Some of these directions are potentially mutually exclusive. For example, having a stateful server may make optimistic UI practically more difficult because a stateful server means that non-serializable state could start to creep in to Mesop applications which makes undoing optimistic UI updates tricky

There's, of course, even more directions than what I've listed here. For example, it's technically possible to compile Python into WebAssembly and run it in the browser and this could be another way of tackling latency to user interactions. However, this seems like a longer-term exploration, which is why I've left it out for now.

"},{"location":"blog/2023/12/25/hello-mesop/#interested-in-contributing","title":"Interested in contributing?","text":"

If any of this excites you, please reach out. The easiest way is to raise a GitHub issue and let me know if there's something specific you'd like to contribute.

"},{"location":"blog/2024/01/12/visual-editor/","title":"Visual Editor","text":""},{"location":"blog/2024/01/12/visual-editor/#why","title":"Why?","text":"

As I began discussing Mesop with friends and colleagues, one thing that has come up is the difficulty of teaching and persuading non-frontend engineers to build UIs, even simple ones. CSS, particularly the rules around layout, can be quite challenging and off-putting.

I've developed a new visual editor for Mesop that aims to make UI building more approachable for beginners and more productive for experts.

"},{"location":"blog/2024/01/12/visual-editor/#what","title":"What?","text":"

Let's take a look at the visual editor:

With the visual editor, you can:

  • Add new components into your app
  • Modify existing components
  • Visualize the component tree hierarchy
  • You can inspect existing components on the page by hovering over them and then change them in the editor panel
  • Bring Your Own components. By decorating a Python function with me.component, you've turned it into a Mesop component and you can now add it with the visual editor.

What's exciting about the visual editor is that you aren't locked into it - everytime you change a component with the visual editor, it's modifying the source code directly so you can seamlessly go back forth between a regular text editor and the visual editor to build your Mesop app.

"},{"location":"blog/2024/01/12/visual-editor/#prior-art","title":"Prior Art","text":"

Visual editors (aka WYSIWYG builders) have been around for a long time. Puck is one of the most interesting ones because of a few reasons: 1) it's open-source, 2) it's flexible (e.g. bring your own components) and 3) it's intuitive and easy-to-use.

The main issues I saw with Puck, particularly for Mesop's use case, is that it currently only supports React (and Mesop uses Angular) and Puck saves data whereas I would like Mesop's Visual Editor to directly emit/update code, which I'll explain next.

"},{"location":"blog/2024/01/12/visual-editor/#principles","title":"Principles","text":""},{"location":"blog/2024/01/12/visual-editor/#hybrid-code-not-low-code","title":"Hybrid code (not low-code)","text":"

One of the reasons why WYSIWYG builders have not gotten much traction with engineers is that they're often good for simple applications, but then you hit a wall building more complex applications.

To avoid this issue, I'm focusing on making the Visual Editor actually emit code and not just data. Essentially, the UI code that you produce from the Visual Editor should be the same as the code that you would write by hand.

"},{"location":"blog/2024/01/12/visual-editor/#unobtrustive-ui","title":"Unobtrustive UI","text":"

I want Mesop app developers to do most of their work (except for the final finetuning for deployment) in the Visual Editior which means that it's important the Editor UI is un-obtrusive. Chrome DevTools is a great example of a low-key tool that many web developers keep open throughout their development - it's helpful for debugging, but then it's out of your way as you're interacting with the application.

Concretely, this means:

  • Editor UI should be collapsible
  • You should be able to \"disable\" the editor mode and interact with the application as a normal user.
"},{"location":"blog/2024/01/12/visual-editor/#contextual","title":"Contextual","text":"

The visual editor should provide only the information that you need when you need it.

For example, rather than showing all the style properties in the editor panel, which would be quite overwhelming, we only show the style properties that you're using for the selected component.

"},{"location":"blog/2024/01/12/visual-editor/#local-only","title":"Local-only","text":"

Because the Visual Editor relies on editing files in your local filesystem, I want to avoid any accidental usages out in the wild. Concretely, this means that you can only use the Visual Editor in localhost, otherwise the Mesop server will reject the editor edit requests.

"},{"location":"blog/2024/01/12/visual-editor/#whats-next","title":"What's next","text":"

There's still a lot of improvements and polishes I would like to make to the visual editor, but a few high-level ideas that I have are:

  1. Build example applications using the visual editor with a video walkthrough.
  2. Create more high-level components in Mesop Labs, which I'll introduce in an upcoming blog post, to make it even easier to build apps with the visual editor.
  3. Drag and drop components onto the page and within the page. This will provide an intuitive experience for building the UI, literally block by block.
"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/","title":"Is Mesop + Web Components the cure to Front-end fatigue?","text":"

I saw this tweet the other day and couldn't help but chuckle:

is this the thing that will finally save me from ever learning front end?https://t.co/eDgY0AfG6U

\u2014 xlr8harder (@xlr8harder) June 6, 2024

At first, I thought of it as joke, but now that Mesop has launched experimental support for Web Components, I think it's plausible that Mesop with Web Components can save you from front-end fatigue.

"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/#what-is-mesop","title":"What is Mesop?","text":"

Before we dive in, let me explain what Mesop is. Mesop is a Python UI framework focused on rapidly building AI apps. You can write a lot of kinds of apps all in Python as you can see from the examples on our home page, but when you need to, Mesop provides the flexibility of dropping down into web components so you can have fine-grained UI control and use existing JS libraries.

"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/#avoid-the-builds","title":"Avoid the builds","text":"

DHH, creator of Rails, recently gave an interview saying how he's \"done with bundling\" and the overall complexity of modern front-end build toolchains.

As someone who's done front-end for almost a decade, I can attest to the sentiment of feeling the pain of compiling JavaScript options. Setting up compiler configs and options can easily take hours. I want to be clear, I think a lot of these tools like TypeScript are fantastic, and the core Mesop framework itself is compiled using TypeScript and Angular's compilers.

But when it comes to rapid prototyping, I want to avoid that overhead. In our design proposal, we intentionally designed a lightweight model where you don't need to set up a complex build chain to start writing JavaScript.

Sometimes a build step is unavoidable, e.g. you're writing TypeScript, and you can still compile your JavaScript as needed.

"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/#framework-churn","title":"Framework churn","text":"

The front-end ecosystem is infamous for its steady and constant churn. The good thing about building on top of web components is that it's based on web standards supported by all modern browsers. This means, that given browser makers' focus on \"not breaking the web\", this will be there for many years, if not decades to come.

For years, web components had a reputation of being an immature technology due to inconsistent support across browsers, but fast forward to 2024, and web components are well-supported in modern browsers and libraries built on web components like Lit, which is downloaded millions of times a week.

"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/#minimizing-front-end-fatigue-in-mesop","title":"Minimizing front-end fatigue in Mesop","text":"

FE developers are so used to the pain and complexity of front-end development that they can forget how steep the learning curve is until someone from another domain tries to build a simple web app, and struggles with just getting the web app up and started.

Mesop app developers are mostly not front-end developers which means that reducing the complexity, especially learning curve, of building custom components is very important. In Mesop, we've designed a smooth pathway where you can get started with a little front-end knowledge and build simple custom components without learning a complex front-end framework.

"},{"location":"blog/2024/07/12/is-mesop--web-components-the-cure-to-front-end-fatigue/#whats-next","title":"What's next","text":"

Follow our X/Twitter account, @mesop_dev for more updates. We're working on improving our web component support, in particular by:

  • Creating guides for wrapping React components into Mesop web components
  • Fostering an ecosystem of open-source Mesop web components by making it easy to discover and reuse web components that other people have built.

We're excited about the potential of Mesop and Web Components to simplify front-end development. Whether it's the cure for front-end fatigue remains to be seen, but I think it offers a promising alternative to the complexity of traditional front-end development.

"},{"location":"blog/2024/05/13/why-mesop/","title":"Why Mesop?","text":"

Mesop is a new UI framework that enables Python developers to quickly build delightful web apps in a scalable way.

Many Python UI frameworks are easy to get started with, but customizing beyond the defaults often requires diving into JavaScript, CSS, and HTML \u2014 a steep learning curve for many developers.

Mesop provides a different approach, offering a framework that's both easy to learn and enables flexible UI building, all within Python.

I want to share a couple concrete ways in which Mesop achieves this.

"},{"location":"blog/2024/05/13/why-mesop/#build-uis-with-functions-ie-components","title":"Build UIs with Functions (i.e. Components)","text":"

Mesop embraces a component-based philosophy where the entire UI is composed of reusable, building blocks which are called components. Using a component is as simple as calling a Python function. This approach offers several benefits:

  • Simplicity: You can use your existing Python knowledge to build UIs quickly and intuitively since components are just functions.
  • Maintainability: Complex UIs become easier to manage and understand by breaking them down into smaller, focused components.
  • Modularity: Components are self-contained, enabling easy reuse within a project or across different projects.

Here's an example of a reusable icon button component:

def icon_button(*, icon: str, label: str, tooltip: str, on_click: Callable):\n  \"\"\"Icon button with text and tooltip.\"\"\"\n  with me.content_button(on_click=on_click):\n    with me.tooltip(message=tooltip):\n      with me.box(style=me.Style(display=\"flex\")):\n        me.icon(icon=icon)\n        me.text(\n          label, style=me.Style(line_height=\"24px\", margin=me.Margin(left=5))\n        )\n
"},{"location":"blog/2024/05/13/why-mesop/#flexibility-through-layered-building-blocks","title":"Flexibility through Layered Building Blocks","text":"

Mesop provides a range of UI building blocks, from low-level native components to high-level components.

  • Low-level components: like box, offer granular control over layout and styling. They empower you to create custom UI elements through flexible layouts like flexbox and grid.
  • High-level components: like chat, are built from low-level components and provide ready-to-use elements for common use cases, enabling rapid development.

This layered approach makes deep customization possible. This means that if you want to customize the chat component, you can fork the chat implementation because it's written entirely in Python using Mesop's public APIs.

"},{"location":"blog/2024/05/13/why-mesop/#see-mesop-in-action","title":"See Mesop in Action","text":"

To demonstrate the range of UIs possible with Mesop, we built a demo gallery to showcase the types of applications you can build and the components that are available:

The demo gallery itself is a Mesop app and implemented in a few hundred lines of Python code. It demonstrates how Mesop can be used to create polished, custom UIs in a maintainable way.

"},{"location":"blog/2024/05/13/why-mesop/#try-mesop","title":"Try Mesop","text":"

If this sounds intriguing, read the Getting Started guide and try building your own Mesop app. Share your feedback and contribute as we continue developing Mesop.

"},{"location":"codelab/","title":"Mesop DuoChat Codelab","text":"

This tutorial walks you through building DuoChat, an interactive web application for chatting with multiple AI models simultaneously. You'll learn how to leverage Mesop's powerful features to create a responsive UI and integrate with AI APIs like Google Gemini and Anthropic Claude.

"},{"location":"codelab/#what-you-will-build","title":"What you will build","text":"

By the end of this codelab, you will build DuoChat (demo) that will allow users to:

  • Select multiple AI models to chat with
  • Compare responses from different models side-by-side
  • Provide their own API keys

If you want to dive straight into the code, you can look at the DuoChat repo and each branch represents the completed code after each section.

"},{"location":"codelab/#setting-up-the-development-environment","title":"Setting Up the Development Environment","text":"

Let's start by setting up our development environment:

  1. Create a new directory for your project:
mkdir duochat\ncd duochat\n
  1. Follow the Mesop command-line installation guide and create a virtual environment and activate it.

  2. Create a requirements.txt file with the following content:

mesop\ngunicorn\nanthropic\ngoogle-generativeai\n
  1. Install the dependencies:
pip install -r requirements.txt\n
"},{"location":"codelab/#setting-up-the-main-application","title":"Setting Up the Main Application","text":"

Let's start by creating a basic Mesop application. Create main.py and add the following code:

main.py
import mesop as me\n\n@me.page(path=\"/\")\ndef page():\n    me.text(\"Welcome to DuoChat!\")\n

This creates a simple Mesop application with a welcome message.

"},{"location":"codelab/#running-the-application","title":"Running the Application","text":"

To run your Mesop application:

mesop main.py\n

Navigate to http://localhost:32123 in your web browser. You should see the welcome message.

"},{"location":"codelab/#getting-api-keys","title":"Getting API keys","text":"

Later on, you will need API keys to call the respective AI models:

  • Get a Google Gemini API Key and use the Gemini API free tier.
  • Get an Anthropic API Key and setup billing. Check their docs for pricing.

TIP: You can get started with the Gemini API key, which has a free tier, first and create the Anthropic API key later.

"},{"location":"codelab/#troubleshooting","title":"Troubleshooting","text":"

If you're having trouble, compare your code to the solution.

"},{"location":"codelab/#next-steps","title":"Next Steps","text":"

In the next section, we'll start building the user interface for DuoChat, including the header, chat input area, and basic styling. We'll explore Mesop's components and styling system to create an attractive and functional layout.

Building the basic UI

"},{"location":"codelab/2/","title":"DuoChat Codelab Part 2: Building the basic UI","text":"

In this section, we'll create the main layout for our DuoChat application, including the header, chat input area, and some basic styling. We'll use Mesop's components and styling system to create an attractive and functional UI.

"},{"location":"codelab/2/#updating-the-main-layout","title":"Updating the Main Layout","text":"

Let's start by updating our main.py file to include a more structured layout. We'll use Mesop's box component for layout and add some custom styles.

Replace the content of main.py with the following:

main.py
import mesop as me\n\nROOT_BOX_STYLE = me.Style(\n    background=\"#e7f2ff\",\n    height=\"100%\",\n    font_family=\"Inter\",\n    display=\"flex\",\n    flex_direction=\"column\",\n)\n\n@me.page(\n    path=\"/\",\n    stylesheets=[\n        \"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap\"\n    ],\n)\ndef page():\n    with me.box(style=ROOT_BOX_STYLE):\n        header()\n        with me.box(\n            style=me.Style(\n                width=\"min(680px, 100%)\",\n                margin=me.Margin.symmetric(\n                    horizontal=\"auto\",\n                    vertical=36,\n                ),\n            )\n        ):\n            me.text(\n                \"Chat with multiple models at once\",\n                style=me.Style(\n                    font_size=20,\n                    margin=me.Margin(bottom=24),\n                ),\n            )\n            chat_input()\n\ndef header():\n    with me.box(\n        style=me.Style(\n            padding=me.Padding.all(16),\n        ),\n    ):\n        me.text(\n            \"DuoChat\",\n            style=me.Style(\n                font_weight=500,\n                font_size=24,\n                color=\"#3D3929\",\n                letter_spacing=\"0.3px\",\n            ),\n        )\n\ndef chat_input():\n    with me.box(\n        style=me.Style(\n            border_radius=16,\n            padding=me.Padding.all(8),\n            background=\"white\",\n            display=\"flex\",\n            width=\"100%\",\n        )\n    ):\n        with me.box(style=me.Style(flex_grow=1)):\n            me.native_textarea(\n                placeholder=\"Enter a prompt\",\n                style=me.Style(\n                    padding=me.Padding(top=16, left=16),\n                    outline=\"none\",\n                    width=\"100%\",\n                    border=me.Border.all(me.BorderSide(style=\"none\")),\n                ),\n            )\n        with me.content_button(type=\"icon\"):\n            me.icon(\"send\")\n

Run the Mesop app and look at the changes:

mesop main.py\n

Let's review the changes:

  1. We've added a ROOT_BOX_STYLE to set the overall layout and background color.
  2. We're importing a custom font (Inter) using the stylesheets parameter in the @me.page decorator.
  3. We've created separate functions for the header and chat_input components.
  4. The main layout uses nested box components with custom styles to create a centered, responsive design.
"},{"location":"codelab/2/#understanding-mesops-styling-system","title":"Understanding Mesop's Styling System","text":"

Mesop's styling system is based on Python classes that correspond to CSS properties. You can learn more by reading the Style API docs.

"},{"location":"codelab/2/#adding-interactivity","title":"Adding Interactivity","text":"

Now, let's add some basic interactivity to our chat input. We'll update the chat_input function to handle user input:

main.py
@me.stateclass\nclass State:\n    input: str = \"\"\n\ndef on_blur(e: me.InputBlurEvent):\n    state = me.state(State)\n    state.input = e.value\n\ndef chat_input():\n    state = me.state(State)\n    with me.box(\n        style=me.Style(\n            border_radius=16,\n            padding=me.Padding.all(8),\n            background=\"white\",\n            display=\"flex\",\n            width=\"100%\",\n        )\n    ):\n        with me.box(style=me.Style(flex_grow=1)):\n            me.native_textarea(\n                value=state.input,\n                placeholder=\"Enter a prompt\",\n                on_blur=on_blur,\n                style=me.Style(\n                    padding=me.Padding(top=16, left=16),\n                    outline=\"none\",\n                    width=\"100%\",\n                    border=me.Border.all(me.BorderSide(style=\"none\")),\n                ),\n            )\n        with me.content_button(type=\"icon\", on_click=send_prompt):\n            me.icon(\"send\")\n\ndef send_prompt(e: me.ClickEvent):\n    state = me.state(State)\n    print(f\"Sending prompt: {state.input}\")\n    state.input = \"\"\n

Here's what we've added:

  1. A State class to manage the application state, including the user's input.
  2. An on_blur function to update the state when the user switches focus from the textarea.
  3. A send_prompt function that will be called when the send button is clicked.
"},{"location":"codelab/2/#running-the-updated-application","title":"Running the Updated Application","text":"

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a styled header, a centered layout, and a functional chat input area.

"},{"location":"codelab/2/#troubleshooting","title":"Troubleshooting","text":"

If you're having trouble, compare your code to the solution.

"},{"location":"codelab/2/#next-steps","title":"Next Steps","text":"

In the next section, we'll dive deeper into state management and implement the model picker dialog.

Managing state & dialogs

"},{"location":"codelab/3/","title":"DuoChat Codelab Part 3: Managing state & dialogs","text":"

In this section, we'll expand our application's state management capabilities and implement a dialog for selecting AI models. We'll use Mesop's state management system and dialog components to create an interactive model selection experience.

"},{"location":"codelab/3/#expanding-the-state-management","title":"Expanding the State Management","text":"

First, let's create a data_model.py file with a more comprehensive state structure:

data_model.py
from dataclasses import dataclass, field\nfrom typing import Literal\nfrom enum import Enum\n\nimport mesop as me\n\nRole = Literal[\"user\", \"model\"]\n\n@dataclass(kw_only=True)\nclass ChatMessage:\n    role: Role = \"user\"\n    content: str = \"\"\n    in_progress: bool = False\n\nclass Models(Enum):\n    GEMINI_1_5_FLASH = \"Gemini 1.5 Flash\"\n    GEMINI_1_5_PRO = \"Gemini 1.5 Pro\"\n    CLAUDE_3_5_SONNET = \"Claude 3.5 Sonnet\"\n\n@dataclass\nclass Conversation:\n    model: str = \"\"\n    messages: list[ChatMessage] = field(default_factory=list)\n\n@me.stateclass\nclass State:\n    is_model_picker_dialog_open: bool = False\n    input: str = \"\"\n    conversations: list[Conversation] = field(default_factory=list)\n    models: list[str] = field(default_factory=list)\n    gemini_api_key: str = \"\"\n    claude_api_key: str = \"\"\n\n@me.stateclass\nclass ModelDialogState:\n    selected_models: list[str] = field(default_factory=list)\n

This expanded state structure allows us to manage multiple conversations, selected models, and API keys.

"},{"location":"codelab/3/#implementing-the-model-picker-dialog","title":"Implementing the Model Picker Dialog","text":"

Now, let's implement the model picker dialog in our main.py file. First, we'll create a new file called dialog.py with the following content, which is based on the dialog pattern from the demo gallery:

dialog.py
import mesop as me\n\n@me.content_component\ndef dialog(is_open: bool):\n    with me.box(\n        style=me.Style(\n            background=\"rgba(0,0,0,0.4)\",\n            display=\"block\" if is_open else \"none\",\n            height=\"100%\",\n            overflow_x=\"auto\",\n            overflow_y=\"auto\",\n            position=\"fixed\",\n            width=\"100%\",\n            z_index=1000,\n        )\n    ):\n        with me.box(\n            style=me.Style(\n                align_items=\"center\",\n                display=\"grid\",\n                height=\"100vh\",\n                justify_items=\"center\",\n            )\n        ):\n            with me.box(\n                style=me.Style(\n                    background=\"#fff\",\n                    border_radius=20,\n                    box_sizing=\"content-box\",\n                    box_shadow=(\n                        \"0 3px 1px -2px #0003, 0 2px 2px #00000024, 0 1px 5px #0000001f\"\n                    ),\n                    margin=me.Margin.symmetric(vertical=\"0\", horizontal=\"auto\"),\n                    padding=me.Padding.all(20),\n                )\n            ):\n                me.slot()\n\n@me.content_component\ndef dialog_actions():\n    with me.box(\n        style=me.Style(\n            display=\"flex\", justify_content=\"end\", margin=me.Margin(top=20)\n        )\n    ):\n        me.slot()\n

Now, let's update our main.py file to include the model picker dialog. Copy the following code and replace main.py with it:

main.py
# Update the imports:\nimport mesop as me\nfrom data_model import State, Models, ModelDialogState\nfrom dialog import dialog, dialog_actions\n\ndef change_model_option(e: me.CheckboxChangeEvent):\n    s = me.state(ModelDialogState)\n    if e.checked:\n        s.selected_models.append(e.key)\n    else:\n        s.selected_models.remove(e.key)\n\ndef set_gemini_api_key(e: me.InputBlurEvent):\n    me.state(State).gemini_api_key = e.value\n\ndef set_claude_api_key(e: me.InputBlurEvent):\n    me.state(State).claude_api_key = e.value\n\ndef model_picker_dialog():\n    state = me.state(State)\n    with dialog(state.is_model_picker_dialog_open):\n        with me.box(style=me.Style(display=\"flex\", flex_direction=\"column\", gap=12)):\n            me.text(\"API keys\")\n            me.input(\n                label=\"Gemini API Key\",\n                value=state.gemini_api_key,\n                on_blur=set_gemini_api_key,\n            )\n            me.input(\n                label=\"Claude API Key\",\n                value=state.claude_api_key,\n                on_blur=set_claude_api_key,\n            )\n        me.text(\"Pick a model\")\n        for model in Models:\n            if model.name.startswith(\"GEMINI\"):\n                disabled = not state.gemini_api_key\n            elif model.name.startswith(\"CLAUDE\"):\n                disabled = not state.claude_api_key\n            else:\n                disabled = False\n            me.checkbox(\n                key=model.value,\n                label=model.value,\n                checked=model.value in state.models,\n                disabled=disabled,\n                on_change=change_model_option,\n                style=me.Style(\n                    display=\"flex\",\n                    flex_direction=\"column\",\n                    gap=4,\n                    padding=me.Padding(top=12),\n                ),\n            )\n        with dialog_actions():\n            me.button(\"Cancel\", on_click=close_model_picker_dialog)\n            me.button(\"Confirm\", on_click=confirm_model_picker_dialog)\n\ndef close_model_picker_dialog(e: me.ClickEvent):\n    state = me.state(State)\n    state.is_model_picker_dialog_open = False\n\ndef confirm_model_picker_dialog(e: me.ClickEvent):\n    dialog_state = me.state(ModelDialogState)\n    state = me.state(State)\n    state.is_model_picker_dialog_open = False\n    state.models = dialog_state.selected_models\n\nROOT_BOX_STYLE = me.Style(\n    background=\"#e7f2ff\",\n    height=\"100%\",\n    font_family=\"Inter\",\n    display=\"flex\",\n    flex_direction=\"column\",\n)\n\n@me.page(\n    path=\"/\",\n    stylesheets=[\n        \"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap\"\n    ],\n)\ndef page():\n    model_picker_dialog()\n    with me.box(style=ROOT_BOX_STYLE):\n        header()\n        with me.box(\n            style=me.Style(\n                width=\"min(680px, 100%)\",\n                margin=me.Margin.symmetric(horizontal=\"auto\", vertical=36),\n            )\n        ):\n            me.text(\n                \"Chat with multiple models at once\",\n                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),\n            )\n            chat_input()\n\ndef header():\n    with me.box(\n        style=me.Style(\n            padding=me.Padding.all(16),\n        ),\n    ):\n        me.text(\n            \"DuoChat\",\n            style=me.Style(\n                font_weight=500,\n                font_size=24,\n                color=\"#3D3929\",\n                letter_spacing=\"0.3px\",\n            ),\n        )\n\ndef switch_model(e: me.ClickEvent):\n    state = me.state(State)\n    state.is_model_picker_dialog_open = True\n    dialog_state = me.state(ModelDialogState)\n    dialog_state.selected_models = state.models[:]\n\ndef chat_input():\n    state = me.state(State)\n    with me.box(\n        style=me.Style(\n            border_radius=16,\n            padding=me.Padding.all(8),\n            background=\"white\",\n            display=\"flex\",\n            width=\"100%\",\n        )\n    ):\n        with me.box(style=me.Style(flex_grow=1)):\n            me.native_textarea(\n                value=state.input,\n                placeholder=\"Enter a prompt\",\n                on_blur=on_blur,\n                style=me.Style(\n                    padding=me.Padding(top=16, left=16),\n                    outline=\"none\",\n                    width=\"100%\",\n                    border=me.Border.all(me.BorderSide(style=\"none\")),\n                ),\n            )\n            with me.box(\n                style=me.Style(\n                    display=\"flex\",\n                    padding=me.Padding(left=12, bottom=12),\n                    cursor=\"pointer\",\n                ),\n                on_click=switch_model,\n            ):\n                me.text(\n                    \"Model:\",\n                    style=me.Style(font_weight=500, padding=me.Padding(right=6)),\n                )\n                if state.models:\n                    me.text(\", \".join(state.models))\n                else:\n                    me.text(\"(no model selected)\")\n        with me.content_button(\n            type=\"icon\", on_click=send_prompt, disabled=not state.models\n        ):\n            me.icon(\"send\")\n\ndef on_blur(e: me.InputBlurEvent):\n    state = me.state(State)\n    state.input = e.value\n\ndef send_prompt(e: me.ClickEvent):\n    state = me.state(State)\n    print(f\"Sending prompt: {state.input}\")\n    print(f\"Selected models: {state.models}\")\n    state.input = \"\"\n

This updated code adds the following features:

  1. A model picker dialog that allows users to select AI models and enter API keys.
  2. State management for selected models and API keys.
  3. A model switcher in the chat input area that opens the model picker dialog.
  4. Disabling of models based on whether the corresponding API key has been entered.
"},{"location":"codelab/3/#running-the-updated-application","title":"Running the Updated Application","text":"

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now see a chat input area with a model switcher. Clicking on the model switcher will open the model picker dialog, allowing you to select models and enter API keys.

"},{"location":"codelab/3/#troubleshooting","title":"Troubleshooting","text":"

If you're having trouble, compare your code to the solution.

"},{"location":"codelab/3/#next-steps","title":"Next Steps","text":"

In the next section, we'll integrate multiple AI models into our application. We'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a way to interact with multiple models.

Integrating AI APIs

"},{"location":"codelab/4/","title":"DuoChat Codelab Part 4: Integrating AI APIs","text":"

In this section, we'll set up connections to Gemini and Claude, implement model-specific chat functions, and create a unified interface for interacting with multiple models simultaneously.

"},{"location":"codelab/4/#setting-up-ai-model-connections","title":"Setting Up AI Model Connections","text":"

First, let's create separate files for our Gemini and Claude integrations.

Create a new file called gemini.py:

gemini.py
import google.generativeai as genai\nfrom typing import Iterable\n\nfrom data_model import ChatMessage, State\nimport mesop as me\n\ngeneration_config = {\n    \"temperature\": 1,\n    \"top_p\": 0.95,\n    \"top_k\": 64,\n    \"max_output_tokens\": 8192,\n}\n\ndef configure_gemini():\n    state = me.state(State)\n    genai.configure(api_key=state.gemini_api_key)\n\ndef send_prompt_pro(prompt: str, history: list[ChatMessage]) -> Iterable[str]:\n    configure_gemini()\n    model = genai.GenerativeModel(\n        model_name=\"gemini-1.5-pro-latest\",\n        generation_config=generation_config,\n    )\n    chat_session = model.start_chat(\n        history=[\n            {\"role\": message.role, \"parts\": [message.content]} for message in history\n        ]\n    )\n    for chunk in chat_session.send_message(prompt, stream=True):\n        yield chunk.text\n\ndef send_prompt_flash(prompt: str, history: list[ChatMessage]) -> Iterable[str]:\n    configure_gemini()\n    model = genai.GenerativeModel(\n        model_name=\"gemini-1.5-flash-latest\",\n        generation_config=generation_config,\n    )\n    chat_session = model.start_chat(\n        history=[\n            {\"role\": message.role, \"parts\": [message.content]} for message in history\n        ]\n    )\n    for chunk in chat_session.send_message(prompt, stream=True):\n        yield chunk.text\n

Now, create a new file called claude.py:

claude.py
import anthropic\nfrom typing import Iterable\n\nfrom data_model import ChatMessage, State\nimport mesop as me\n\ndef call_claude_sonnet(input: str, history: list[ChatMessage]) -> Iterable[str]:\n    state = me.state(State)\n    client = anthropic.Anthropic(api_key=state.claude_api_key)\n    messages = [\n        {\n            \"role\": \"assistant\" if message.role == \"model\" else message.role,\n            \"content\": message.content,\n        }\n        for message in history\n    ] + [{\"role\": \"user\", \"content\": input}]\n\n    with client.messages.stream(\n        max_tokens=1024,\n        messages=messages,\n        model=\"claude-3-sonnet-20240229\",\n    ) as stream:\n        for text in stream.text_stream:\n            yield text\n
"},{"location":"codelab/4/#updating-the-main-application","title":"Updating the Main Application","text":"

Now, let's update our main.py file to integrate these AI models. We'll update the page function, add the display_conversations and display_message functions and modify the send_prompt function to handle multiple models and display their responses:

main.py
import mesop as me\nfrom data_model import State, Models, ModelDialogState, Conversation, ChatMessage\nfrom dialog import dialog, dialog_actions\nimport claude\nimport gemini\n\n# ... (keep the existing imports and styles)\n\n# Replace page() with this:\n@me.page(\n    path=\"/\",\n    stylesheets=[\n        \"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap\"\n    ],\n)\ndef page():\n    model_picker_dialog()\n    with me.box(style=ROOT_BOX_STYLE):\n        header()\n        with me.box(\n            style=me.Style(\n                width=\"min(680px, 100%)\",\n                margin=me.Margin.symmetric(horizontal=\"auto\", vertical=36),\n            )\n        ):\n            me.text(\n                \"Chat with multiple models at once\",\n                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),\n            )\n            chat_input()\n            display_conversations()\n\n# Add display_conversations and display_message:\ndef display_conversations():\n    state = me.state(State)\n    for conversation in state.conversations:\n        with me.box(style=me.Style(margin=me.Margin(bottom=24))):\n            me.text(f\"Model: {conversation.model}\", style=me.Style(font_weight=500))\n            for message in conversation.messages:\n                display_message(message)\n\ndef display_message(message: ChatMessage):\n    style = me.Style(\n        padding=me.Padding.all(12),\n        border_radius=8,\n        margin=me.Margin(bottom=8),\n    )\n    if message.role == \"user\":\n        style.background = \"#e7f2ff\"\n    else:\n        style.background = \"#ffffff\"\n\n    with me.box(style=style):\n        me.markdown(message.content)\n        if message.in_progress:\n            me.progress_spinner()\n\n# Update send_prompt:\ndef send_prompt(e: me.ClickEvent):\n    state = me.state(State)\n    if not state.conversations:\n        for model in state.models:\n            state.conversations.append(Conversation(model=model, messages=[]))\n    input = state.input\n    state.input = \"\"\n\n    for conversation in state.conversations:\n        model = conversation.model\n        messages = conversation.messages\n        history = messages[:]\n        messages.append(ChatMessage(role=\"user\", content=input))\n        messages.append(ChatMessage(role=\"model\", in_progress=True))\n        yield\n\n        if model == Models.GEMINI_1_5_FLASH.value:\n            llm_response = gemini.send_prompt_flash(input, history)\n        elif model == Models.GEMINI_1_5_PRO.value:\n            llm_response = gemini.send_prompt_pro(input, history)\n        elif model == Models.CLAUDE_3_5_SONNET.value:\n            llm_response = claude.call_claude_sonnet(input, history)\n        else:\n            raise Exception(\"Unhandled model\", model)\n\n        for chunk in llm_response:\n            messages[-1].content += chunk\n            yield\n        messages[-1].in_progress = False\n        yield\n

This updated code adds the following features:

  1. A display_conversations function that shows the chat history for each selected model.
  2. A display_message function that renders individual messages with appropriate styling.
  3. An updated send_prompt function that sends the user's input to all selected models and displays their responses in real-time.
"},{"location":"codelab/4/#handling-streaming-responses","title":"Handling Streaming Responses","text":"

The send_prompt function now uses Python generators to handle streaming responses from the AI models. This allows us to update the UI in real-time as the models generate their responses.

"},{"location":"codelab/4/#running-the-updated-application","title":"Running the Updated Application","text":"

Run the application again with mesop main.py and navigate to http://localhost:32123. You should now be able to:

  1. Select multiple AI models using the model picker dialog.
  2. Enter API keys for Gemini and Claude.
  3. Send prompts to the selected models.
  4. See the responses from multiple models displayed simultaneously.
"},{"location":"codelab/4/#troubleshooting","title":"Troubleshooting","text":"

If you're having trouble, compare your code to the solution.

"},{"location":"codelab/4/#next-steps","title":"Next Steps","text":"

In the final section, we'll refine the user interface by creating a dedicated conversation page with a multi-column layout for different model responses. We'll also add some finishing touches to improve the overall user experience.

Wrapping it up

"},{"location":"codelab/5/","title":"DuoChat Codelab Part 5: Wrapping it up","text":"

In this section, we'll create a multi-column layout for different model responses, implement user and model message components, add auto-scroll functionality, and finalize the chat experience with multiple models.

"},{"location":"codelab/5/#creating-a-new-conversation-page","title":"Creating a New Conversation Page","text":"

First, let's create a new page for our conversations. Update the main.py file to include a new route and function for the conversation page:

main.py
import mesop as me\nfrom data_model import State, Models, ModelDialogState, Conversation, ChatMessage\nfrom dialog import dialog, dialog_actions\nimport claude\nimport gemini\n\n# ... (keep the existing imports and styles)\n\nSTYLESHEETS = [\n  \"https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap\"\n]\n\n@me.page(\n    path=\"/\",\n    stylesheets=STYLESHEETS,\n)\ndef home_page():\n    model_picker_dialog()\n    with me.box(style=ROOT_BOX_STYLE):\n        header()\n        with me.box(\n            style=me.Style(\n                width=\"min(680px, 100%)\",\n                margin=me.Margin.symmetric(horizontal=\"auto\", vertical=36),\n            )\n        ):\n            me.text(\n                \"Chat with multiple models at once\",\n                style=me.Style(font_size=20, margin=me.Margin(bottom=24)),\n            )\n            # Uncomment this in the next step:\n            # examples_row()\n            chat_input()\n\n@me.page(path=\"/conversation\", stylesheets=STYLESHEETS)\ndef conversation_page():\n    state = me.state(State)\n    model_picker_dialog()\n    with me.box(style=ROOT_BOX_STYLE):\n        header()\n\n        models = len(state.conversations)\n        models_px = models * 680\n        with me.box(\n            style=me.Style(\n                width=f\"min({models_px}px, calc(100% - 32px))\",\n                display=\"grid\",\n                gap=16,\n                grid_template_columns=f\"repeat({models}, 1fr)\",\n                flex_grow=1,\n                overflow_y=\"hidden\",\n                margin=me.Margin.symmetric(horizontal=\"auto\"),\n                padding=me.Padding.symmetric(horizontal=16),\n            )\n        ):\n            for conversation in state.conversations:\n                model = conversation.model\n                messages = conversation.messages\n                with me.box(\n                    style=me.Style(\n                        overflow_y=\"auto\",\n                    )\n                ):\n                    me.text(\"Model: \" + model, style=me.Style(font_weight=500))\n\n                    for message in messages:\n                        if message.role == \"user\":\n                            user_message(message.content)\n                        else:\n                            model_message(message)\n                    if messages and model == state.conversations[-1].model:\n                        me.box(\n                            key=\"end_of_messages\",\n                            style=me.Style(\n                                margin=me.Margin(\n                                    bottom=\"50vh\" if messages[-1].in_progress else 0\n                                )\n                            ),\n                        )\n        with me.box(\n            style=me.Style(\n                display=\"flex\",\n                justify_content=\"center\",\n            )\n        ):\n            with me.box(\n                style=me.Style(\n                    width=\"min(680px, 100%)\",\n                    padding=me.Padding(top=24, bottom=24),\n                )\n            ):\n                chat_input()\n\ndef user_message(content: str):\n    with me.box(\n        style=me.Style(\n            background=\"#e7f2ff\",\n            padding=me.Padding.all(16),\n            margin=me.Margin.symmetric(vertical=16),\n            border_radius=16,\n        )\n    ):\n        me.text(content)\n\ndef model_message(message: ChatMessage):\n    with me.box(\n        style=me.Style(\n            background=\"#fff\",\n            padding=me.Padding.all(16),\n            border_radius=16,\n            margin=me.Margin.symmetric(vertical=16),\n        )\n    ):\n        me.markdown(message.content)\n        if message.in_progress:\n            me.progress_spinner()\n\n# ... (keep the existing helper functions)\n\ndef send_prompt(e: me.ClickEvent):\n    state = me.state(State)\n    if not state.conversations:\n        me.navigate(\"/conversation\")\n        for model in state.models:\n            state.conversations.append(Conversation(model=model, messages=[]))\n    input = state.input\n    state.input = \"\"\n\n    for conversation in state.conversations:\n        model = conversation.model\n        messages = conversation.messages\n        history = messages[:]\n        messages.append(ChatMessage(role=\"user\", content=input))\n        messages.append(ChatMessage(role=\"model\", in_progress=True))\n        yield\n        me.scroll_into_view(key=\"end_of_messages\")\n        if model == Models.GEMINI_1_5_FLASH.value:\n            llm_response = gemini.send_prompt_flash(input, history)\n        elif model == Models.GEMINI_1_5_PRO.value:\n            llm_response = gemini.send_prompt_pro(input, history)\n        elif model == Models.CLAUDE_3_5_SONNET.value:\n            llm_response = claude.call_claude_sonnet(input, history)\n        else:\n            raise Exception(\"Unhandled model\", model)\n        for chunk in llm_response:\n            messages[-1].content += chunk\n            yield\n        messages[-1].in_progress = False\n        yield\n

Try running the app: mesop main.py and now you should navigate to the conversation page once you click the send button.

"},{"location":"codelab/5/#adding-example-prompts","title":"Adding Example Prompts","text":"

Let's add some example prompts to the home page to help users get started. Add the following functions to main.py:

main.py
EXAMPLES = [\n    \"Create a file-lock in Python\",\n    \"Write an email to Congress to have free milk for all\",\n    \"Make a nice box shadow in CSS\",\n]\n\ndef examples_row():\n    with me.box(\n        style=me.Style(\n            display=\"flex\", flex_direction=\"row\", gap=16, margin=me.Margin(bottom=24)\n        )\n    ):\n        for i in EXAMPLES:\n            example(i)\n\ndef example(text: str):\n    with me.box(\n        key=text,\n        on_click=click_example,\n        style=me.Style(\n            cursor=\"pointer\",\n            background=\"#b9e1ff\",\n            width=\"215px\",\n            height=160,\n            font_weight=500,\n            line_height=\"1.5\",\n            padding=me.Padding.all(16),\n            border_radius=16,\n            border=me.Border.all(me.BorderSide(width=1, color=\"blue\", style=\"none\")),\n        ),\n    ):\n        me.text(text)\n\ndef click_example(e: me.ClickEvent):\n    state = me.state(State)\n    state.input = e.key\n

And then uncomment the callsite for examples_row in home_page.

"},{"location":"codelab/5/#updating-the-header","title":"Updating the Header","text":"

Let's update header to allow users to return to the home page when they click on the header box:

main.py
def header():\n    def navigate_home(e: me.ClickEvent):\n        me.navigate(\"/\")\n        state = me.state(State)\n        state.conversations = []\n\n    with me.box(\n        on_click=navigate_home,\n        style=me.Style(\n            cursor=\"pointer\",\n            padding=me.Padding.all(16),\n        ),\n    ):\n        me.text(\n            \"DuoChat\",\n            style=me.Style(\n                font_weight=500,\n                font_size=24,\n                color=\"#3D3929\",\n                letter_spacing=\"0.3px\",\n            ),\n        )\n
"},{"location":"codelab/5/#finalizing-the-chat-experience","title":"Finalizing the Chat Experience","text":"

Now that we have a dedicated conversation page with a multi-column layout, let's make some final improvements to enhance the user experience:

  1. Navigate to the conversations page at the start of a conversation.
  2. Implement auto-scrolling to keep the latest messages in view.

Update the send_prompt function in main.py:

main.py
def send_prompt(e: me.ClickEvent):\n    state = me.state(State)\n    if not state.conversations:\n        me.navigate(\"/conversation\")\n        for model in state.models:\n            state.conversations.append(Conversation(model=model, messages=[]))\n    input = state.input\n    state.input = \"\"\n\n    for conversation in state.conversations:\n        model = conversation.model\n        messages = conversation.messages\n        history = messages[:]\n        messages.append(ChatMessage(role=\"user\", content=input))\n        messages.append(ChatMessage(role=\"model\", in_progress=True))\n        yield\n        me.scroll_into_view(key=\"end_of_messages\")\n        if model == Models.GEMINI_1_5_FLASH.value:\n            llm_response = gemini.send_prompt_flash(input, history)\n        elif model == Models.GEMINI_1_5_PRO.value:\n            llm_response = gemini.send_prompt_pro(input, history)\n        elif model == Models.CLAUDE_3_5_SONNET.value:\n            llm_response = claude.call_claude_sonnet(input, history)\n        else:\n            raise Exception(\"Unhandled model\", model)\n        for chunk in llm_response:\n            messages[-1].content += chunk\n            yield\n        messages[-1].in_progress = False\n        yield\n
"},{"location":"codelab/5/#running-the-final-application","title":"Running the Final Application","text":"

Run the application with mesop main.py and navigate to http://localhost:32123. You should now have a fully functional DuoChat application with the following features:

  1. A home page with example prompts and a model picker.
  2. A conversation page with a multi-column layout for different model responses.
  3. Real-time streaming of model responses with loading indicators.
  4. Auto-scrolling to keep the latest message in view.
"},{"location":"codelab/5/#troubleshooting","title":"Troubleshooting","text":"

If you're having trouble, compare your code to the solution.

"},{"location":"codelab/5/#conclusion","title":"Conclusion","text":"

Congratulations! You've successfully built DuoChat, a Mesop application that allows users to interact with multiple AI models simultaneously. This project demonstrates Mesop's capabilities for creating responsive UIs, managing complex state, and integrating with external APIs.

Some potential next steps to further improve the application:

  1. Deploy your app.
  2. Add the ability to save and load conversations.
  3. Add support for additional AI models or services.

Feel free to experiment with these ideas or come up with your own improvements to enhance the DuoChat experience!

"},{"location":"components/","title":"Components","text":"

Please read Core Concepts before this as it explains the basics of components. This page provides an overview of the different types of components in Mesop.

"},{"location":"components/#types-of-components","title":"Types of components","text":""},{"location":"components/#native-components","title":"Native components","text":"

Native components are components implemented using Angular/Javascript. Many of these components wrap Angular Material components. Other components are simple wrappers around DOM elements.

If you have a use case that's not supported by the existing native components, please file an issue on GitHub to explain your use case. Given our limited bandwidth, we may not be able to build it soon, but in the future, we will enable Mesop developers to build their own custom native components.

"},{"location":"components/#user-defined-components","title":"User-defined components","text":"

User-defined components are essentially Python functions which call other components, which can be native components or other user-defined components. It's very easy to write your own components, and it's encouraged to split your app into modular components for better maintainability and reusability.

"},{"location":"components/#web-components","title":"Web components","text":"

Web components in Mesop are custom HTML elements created using JavaScript and CSS. They enable custom JavaScript execution and bi-directional communication between the browser and server. They can wrap JavaScript libraries and provide stateful client-side interactions. Learn more about web components.

"},{"location":"components/#content-components","title":"Content components","text":"

Content components allow you to compose components more flexibly than regular components by accepting child(ren) components. A commonly used content component is the button component, which accepts a child component which oftentimes the text component.

Example:

with me.button():\n  me.text(\"Child\")\n

You can also have multiple content components nested:

with me.box():\n  with me.box():\n    me.text(\"Grand-child\")\n

Sometimes, you may want to define your own content component for better reusability. For example, let's say I want to define a scaffold component which includes a menu positioned on the left and a main content area, I could do the following:

@me.content_component\ndef scaffold(url: str):\n  with me.box(style=me.Style(background=\"white\")):\n    menu(url=url)\n    with me.box(style=me.Style(padding=me.Padding(left=MENU_WIDTH))):\n      me.slot()\n

Now other components can re-use this scaffold component:

def page1():\n  with scaffold(url=\"/page1\"):\n    some_content(...)\n

This is similar to Angular's Content Projection.

"},{"location":"components/#component-key","title":"Component Key","text":"

Every native component in Mesop accepts a key argument which is a component identifier. This is used by Mesop to tell Angular whether to reuse the DOM element.

"},{"location":"components/#resetting-a-component","title":"Resetting a component","text":"

You can reset a component to the initial state (e.g. reset a select component to the unselected state) by giving it a new key value across renders.

For example, you can reset a component by \"incrementing\" the key:

class State:\n  select_menu_key: int\n\ndef reset(event):\n  state = me.state(State)\n  state.select_menu_key += 1\n\ndef main():\n  state = me.state(State)\n  me.select(key=str(state.select_menu_key),\n            options=[me.SelectOption(label=\"o1\", value=\"o1\")])\n  me.button(label=\"Reset\", on_click=reset)\n
"},{"location":"components/#event-handlers","title":"Event handlers","text":"

Every Mesop event includes the key of the component which emitted the event. This makes it useful when you want to reuse an event handler for multiple instances of a component:

def buttons():\n  for fruit in [\"Apple\", \"Banana\"]:\n    me.button(fruit, key=fruit, on_click=on_click)\n\ndef on_click(event: me.ClickEvent):\n  fruit = event.key\n  print(\"fruit name\", fruit)\n

Because a key is a str type, you may sometimes want to store more complex data like a dataclass or a proto object for retrieval in the event handler. To do this, you can serialize and deserialize:

import json\nfrom dataclasses import dataclass\n\n@dataclass\nclass Person:\n  name: str\n\ndef buttons():\n  for person in [Person(name=\"Alice\"), Person(name=\"Bob\")]:\n    # serialize dataclass into str\n    key = json.dumps(person.asdict())\n    me.button(person.name, key=key, on_click=on_click)\n\ndef on_click(event: me.ClickEvent):\n  person_dict = json.loads(event.key)\n  # modify this for more complex deserialization\n  person = Person(**person_dict)\n

Use component key for reusable event handler

This avoids a subtle issue with using closure variables in event handlers.

"},{"location":"components/audio/","title":"Audio","text":""},{"location":"components/audio/#overview","title":"Overview","text":"

Audio is the equivalent of an <audio> HTML element. Audio displays the browser's native audio controls.

"},{"location":"components/audio/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/audio\",\n)\ndef app():\n  \"\"\"\n  In order to autoplay audio, set the `autoplay` attribute to `True`,\n  Note that there are autoplay restrictions in modern browsers, including Chrome,\n  are designed to prevent audio or video from playing automatically without user interaction.\n  This is intended to improve user experience and reduce unwanted interruptions.\n  You can check the [autoplay ability of your application](https://developer.mozilla.org/en-US/docs/Web/Media/Autoplay_guide#autoplay_availability)\n  \"\"\"\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.audio(\n      src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-audio/t-rex-roar.mp3\",\n      # autoplay=True\n    )\n
"},{"location":"components/audio/#api","title":"API","text":""},{"location":"components/audio/#mesop.components.audio.audio.audio","title":"audio","text":"

Creates an audio component.

PARAMETER DESCRIPTION src

The URL of the audio to be played.

TYPE: str | None DEFAULT: None

autoplay

boolean value indicating if the audio should be autoplayed or not. Note: There are autoplay restrictions in modern browsers, including Chrome, are designed to prevent audio or video from playing automatically without user interaction. This is intended to improve user experience and reduce unwanted interruptions

TYPE: bool DEFAULT: False

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/autocomplete/","title":"Autocomplete","text":""},{"location":"components/autocomplete/#overview","title":"Overview","text":"

Autocomplete allows the user to enter free text or select from a list of dynamic values and is based on the Angular Material autocomplete component.

This components only renders text labels and values.

The autocomplete filters by case-insensitively matching substrings of the option label.

Currently, there is no on blur event with this component since the blur event does not get the selected value on the first blur. Due to this ambiguous behavior, the blur event has been left out.

"},{"location":"components/autocomplete/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  raw_value: str\n  selected_value: str = \"California\"\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/autocomplete\",\n)\ndef app():\n  state = me.state(State)\n\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.autocomplete(\n      label=\"Select state\",\n      value=state.selected_value,\n      options=_make_autocomplete_options(),\n      on_selection_change=on_value_change,\n      on_enter=on_value_change,\n      on_input=on_input,\n      appearance=\"outline\",\n    )\n\n    if state.selected_value:\n      me.text(\"Selected: \" + state.selected_value)\n\n\ndef on_value_change(\n  e: me.AutocompleteEnterEvent | me.AutocompleteSelectionChangeEvent,\n):\n  state = me.state(State)\n  state.selected_value = e.value\n\n\ndef on_input(e: me.InputEvent):\n  state = me.state(State)\n  state.raw_value = e.value\n\n\ndef _make_autocomplete_options() -> list[me.AutocompleteOptionGroup]:\n  \"\"\"Creates and filter autocomplete options.\n\n  The states list assumed to be alphabetized and we group by the first letter of the\n  state's name.\n  \"\"\"\n  states_options_list = []\n  sub_group = None\n  for state in _STATES:\n    if not sub_group or sub_group.label != state[0]:\n      if sub_group:\n        states_options_list.append(sub_group)\n      sub_group = me.AutocompleteOptionGroup(label=state[0], options=[])\n    sub_group.options.append(me.AutocompleteOption(label=state, value=state))\n  if sub_group:\n    states_options_list.append(sub_group)\n  return states_options_list\n\n\n_STATES = [\n  \"Alabama\",\n  \"Alaska\",\n  \"Arizona\",\n  \"Arkansas\",\n  \"California\",\n  \"Colorado\",\n  \"Connecticut\",\n  \"Delaware\",\n  \"Florida\",\n  \"Georgia\",\n  \"Hawaii\",\n  \"Idaho\",\n  \"Illinois\",\n  \"Indiana\",\n  \"Iowa\",\n  \"Kansas\",\n  \"Kentucky\",\n  \"Louisiana\",\n  \"Maine\",\n  \"Maryland\",\n  \"Massachusetts\",\n  \"Michigan\",\n  \"Minnesota\",\n  \"Mississippi\",\n  \"Missouri\",\n  \"Montana\",\n  \"Nebraska\",\n  \"Nevada\",\n  \"New Hampshire\",\n  \"New Jersey\",\n  \"New Mexico\",\n  \"New York\",\n  \"North Carolina\",\n  \"North Dakota\",\n  \"Ohio\",\n  \"Oklahoma\",\n  \"Oregon\",\n  \"Pennsylvania\",\n  \"Rhode Island\",\n  \"South Carolina\",\n  \"South Dakota\",\n  \"Tennessee\",\n  \"Texas\",\n  \"Utah\",\n  \"Vermont\",\n  \"Virginia\",\n  \"Washington\",\n  \"West Virginia\",\n  \"Wisconsin\",\n  \"Wyoming\",\n]\n
"},{"location":"components/autocomplete/#api","title":"API","text":""},{"location":"components/autocomplete/#mesop.components.autocomplete.autocomplete.autocomplete","title":"autocomplete","text":"

Creates an autocomplete component.

PARAMETER DESCRIPTION options

Selectable options from autocomplete.

TYPE: Iterable[AutocompleteOption | AutocompleteOptionGroup] | None DEFAULT: None

label

Label for input.

TYPE: str DEFAULT: ''

on_selection_change

Event emitted when the selected value has been changed by the user.

TYPE: Callable[[AutocompleteSelectionChangeEvent], Any] | None DEFAULT: None

on_input

input is fired whenever the input has changed (e.g. user types).

TYPE: Callable[[InputEvent], Any] | None DEFAULT: None

on_enter

triggers when the browser detects an \"Enter\" key on a keyup native browser event.

TYPE: Callable[[AutocompleteEnterEvent], Any] | None DEFAULT: None

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder

Placeholder value.

TYPE: str DEFAULT: ''

value

Initial value.

TYPE: str DEFAULT: ''

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

hide_required_marker

Whether the required marker should be hidden.

TYPE: bool DEFAULT: False

color

The color palette for the form field.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

float_label

Whether the label should always float or float as the user types.

TYPE: Literal['always', 'auto'] DEFAULT: 'auto'

subscript_sizing

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

TYPE: Literal['fixed', 'dynamic'] DEFAULT: 'fixed'

hint_label

Text for the form field hint.

TYPE: str DEFAULT: ''

style

Style for input.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/autocomplete/#mesop.components.autocomplete.autocomplete.AutocompleteOption","title":"AutocompleteOption dataclass","text":"

Represents an option in the autocomplete drop down.

ATTRIBUTE DESCRIPTION label

Content to show for the autocomplete option

TYPE: str | None

value

The value of this autocomplete option.

TYPE: str | None

"},{"location":"components/autocomplete/#mesop.components.autocomplete.autocomplete.AutocompleteOptionGroup","title":"AutocompleteOptionGroup dataclass","text":"

Represents an option group to group options in the autocomplete drop down.

ATTRIBUTE DESCRIPTION label

Group label

TYPE: str

options

Autocomplete options under this group

TYPE: list[AutocompleteOption]

"},{"location":"components/autocomplete/#mesop.components.autocomplete.autocomplete.AutocompleteEnterEvent","title":"AutocompleteEnterEvent dataclass","text":"

Bases: MesopEvent

Represents an \"Enter\" keyboard event on an autocomplete component.

ATTRIBUTE DESCRIPTION value

Input/selected value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/autocomplete/#mesop.components.autocomplete.autocomplete.AutocompleteSelectionChangeEvent","title":"AutocompleteSelectionChangeEvent dataclass","text":"

Bases: MesopEvent

Represents a selection change event.

ATTRIBUTE DESCRIPTION value

Selected value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/autocomplete/#mesop.events.InputEvent","title":"InputEvent dataclass","text":"

Bases: MesopEvent

Represents a user input event.

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/badge/","title":"Badge","text":""},{"location":"components/badge/#overview","title":"Overview","text":"

Badge decorates a UI component and is oftentimes used for unread message count and is based on the Angular Material badge component.

"},{"location":"components/badge/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/badge\",\n)\ndef app():\n  with me.box(\n    style=me.Style(\n      display=\"block\",\n      padding=me.Padding(top=16, right=16, bottom=16, left=16),\n      height=50,\n      width=30,\n    )\n  ):\n    with me.badge(content=\"1\", size=\"medium\"):\n      me.text(text=\"text with badge\")\n
"},{"location":"components/badge/#api","title":"API","text":""},{"location":"components/badge/#mesop.components.badge.badge.badge","title":"badge","text":"

Creates a Badge component. Badge is a composite component.

PARAMETER DESCRIPTION color

The color of the badge. Can be primary, accent, or warn.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

overlap

Whether the badge should overlap its contents or not

TYPE: bool DEFAULT: False

disabled

Whether the badge is disabled.

TYPE: bool DEFAULT: False

position

Position the badge should reside. Accepts any combination of 'above'|'below' and 'before'|'after'

TYPE: Literal['above after', 'above before', 'below before', 'below after', 'before', 'after', 'above', 'below'] DEFAULT: 'above after'

content

The content for the badge

TYPE: str DEFAULT: ''

description

Message used to describe the decorated element via aria-describedby

TYPE: str DEFAULT: ''

size

Size of the badge. Can be 'small', 'medium', or 'large'.

TYPE: Literal['small', 'medium', 'large'] DEFAULT: 'small'

hidden

Whether the badge is hidden.

TYPE: bool DEFAULT: False

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/box/","title":"Box","text":""},{"location":"components/box/#overview","title":"Overview","text":"

Box is a content component which acts as a container to group children components and styling them.

"},{"location":"components/box/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/box\",\n)\ndef app():\n  with me.box(style=me.Style(background=\"red\", padding=me.Padding.all(16))):\n    with me.box(\n      style=me.Style(\n        background=\"green\",\n        height=50,\n        margin=me.Margin.symmetric(vertical=24, horizontal=12),\n        border=me.Border.symmetric(\n          horizontal=me.BorderSide(width=2, color=\"pink\", style=\"solid\"),\n          vertical=me.BorderSide(width=2, color=\"orange\", style=\"solid\"),\n        ),\n      )\n    ):\n      me.text(text=\"hi1\")\n      me.text(text=\"hi2\")\n\n    with me.box(\n      style=me.Style(\n        background=\"blue\",\n        height=50,\n        margin=me.Margin.all(16),\n        border=me.Border.all(\n          me.BorderSide(width=2, color=\"yellow\", style=\"dotted\")\n        ),\n        border_radius=10,\n      )\n    ):\n      me.text(text=\"Example with all sides bordered\")\n\n    with me.box(\n      style=me.Style(\n        background=\"purple\",\n        height=50,\n        margin=me.Margin.symmetric(vertical=24, horizontal=12),\n        border=me.Border.symmetric(\n          vertical=me.BorderSide(width=4, color=\"white\", style=\"double\")\n        ),\n      )\n    ):\n      me.text(text=\"Example with top and bottom borders\")\n\n    with me.box(\n      style=me.Style(\n        background=\"cyan\",\n        height=50,\n        margin=me.Margin.symmetric(vertical=24, horizontal=12),\n        border=me.Border.symmetric(\n          horizontal=me.BorderSide(width=2, color=\"black\", style=\"groove\")\n        ),\n      )\n    ):\n      me.text(text=\"Example with left and right borders\")\n
"},{"location":"components/box/#api","title":"API","text":""},{"location":"components/box/#mesop.components.box.box.box","title":"box","text":"

Creates a box component.

PARAMETER DESCRIPTION style

Style to apply to component. Follows HTML Element inline style API.

TYPE: Style | None DEFAULT: None

on_click

The callback function that is called when the box is clicked. It receives a ClickEvent as its only argument.

TYPE: Callable[[ClickEvent], Any] | None DEFAULT: None

classes

CSS classes.

TYPE: list[str] | str DEFAULT: ''

key

The component key.

TYPE: str | None DEFAULT: None

RETURNS DESCRIPTION Any

The created box component.

"},{"location":"components/button/","title":"Button","text":""},{"location":"components/button/#overview","title":"Overview","text":"

Button is based on the Angular Material button component.

"},{"location":"components/button/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/button\",\n)\ndef main():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\"Button types:\", style=me.Style(margin=me.Margin(bottom=12)))\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", gap=12)):\n      me.button(\"default\")\n      me.button(\"raised\", type=\"raised\")\n      me.button(\"flat\", type=\"flat\")\n      me.button(\"stroked\", type=\"stroked\")\n\n    me.text(\n      \"Button colors:\", style=me.Style(margin=me.Margin(top=12, bottom=12))\n    )\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", gap=12)):\n      me.button(\"default\", type=\"flat\")\n      me.button(\"primary\", color=\"primary\", type=\"flat\")\n      me.button(\"secondary\", color=\"accent\", type=\"flat\")\n      me.button(\"warn\", color=\"warn\", type=\"flat\")\n
"},{"location":"components/button/#api","title":"API","text":""},{"location":"components/button/#mesop.components.button.button.button","title":"button","text":"

Creates a simple text Button component.

PARAMETER DESCRIPTION label

Text label for button

TYPE: str | None DEFAULT: None

on_click

click is a native browser event.

TYPE: Callable[[ClickEvent], Any] | None DEFAULT: None

type

Type of button style to use

TYPE: Literal['raised', 'flat', 'stroked'] | None DEFAULT: None

color

Theme color palette of the button

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

disable_ripple

Whether the ripple effect is disabled or not.

TYPE: bool DEFAULT: False

disabled

Whether the button is disabled.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/button/#mesop.components.button.button.content_button","title":"content_button","text":"

Creates a button component, which is a composite component. Typically, you would use a text or icon component as a child.

Intended for advanced use cases.

PARAMETER DESCRIPTION on_click

click is a native browser event.

TYPE: Callable[[ClickEvent], Any] | None DEFAULT: None

type

Type of button style to use

TYPE: Literal['raised', 'flat', 'stroked', 'icon'] | None DEFAULT: None

color

Theme color palette of the button

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

disable_ripple

Whether the ripple effect is disabled or not.

TYPE: bool DEFAULT: False

disabled

Whether the button is disabled.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/button/#mesop.events.ClickEvent","title":"ClickEvent dataclass","text":"

Bases: MesopEvent

Represents a user click event.

ATTRIBUTE DESCRIPTION key

key of the component that emitted this event.

TYPE: str

is_target

Whether the clicked target is the component which attached the event handler.

TYPE: bool

"},{"location":"components/button_toggle/","title":"Button toggle","text":""},{"location":"components/button_toggle/#overview","title":"Overview","text":"

Button toggle is based on the Angular Material button toggle component.

"},{"location":"components/button_toggle/#examples","title":"Examples","text":"
from dataclasses import field\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  selected_values: list[str] = field(\n    default_factory=lambda: [\"bold\", \"underline\"]\n  )\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(on_load=load, path=\"/button_toggle\")\ndef app():\n  state = me.state(State)\n\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.button_toggle(\n      value=state.selected_values,\n      buttons=[\n        me.ButtonToggleButton(label=\"Bold\", value=\"bold\"),\n        me.ButtonToggleButton(label=\"Italic\", value=\"italic\"),\n        me.ButtonToggleButton(label=\"Underline\", value=\"underline\"),\n      ],\n      multiple=True,\n      hide_selection_indicator=False,\n      disabled=False,\n      on_change=on_change,\n      style=me.Style(margin=me.Margin(bottom=20)),\n    )\n\n    me.text(\"Select buttons: \" + \" \".join(state.selected_values))\n\n\ndef on_change(e: me.ButtonToggleChangeEvent):\n  state = me.state(State)\n  state.selected_values = e.values\n
"},{"location":"components/button_toggle/#api","title":"API","text":""},{"location":"components/button_toggle/#mesop.components.button_toggle.button_toggle.button_toggle","title":"button_toggle","text":"

This function creates a button toggle.

PARAMETER DESCRIPTION value

Selected values of the button toggle.

TYPE: list[str] | str DEFAULT: ''

buttons

List of button toggles.

TYPE: Iterable[ButtonToggleButton]

on_change

Event emitted when the group's value changes.

TYPE: Callable[[ButtonToggleChangeEvent], Any] | None DEFAULT: None

multiple

Whether multiple button toggles can be selected.

TYPE: bool DEFAULT: False

disabled

Whether multiple button toggle group is disabled.

TYPE: bool DEFAULT: False

hide_selection_indicator

Whether checkmark indicator for button toggle groups is hidden.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/button_toggle/#mesop.components.button_toggle.button_toggle.ButtonToggleButton","title":"ButtonToggleButton dataclass","text":"ATTRIBUTE DESCRIPTION label

Content to show for the button toggle button

TYPE: str | None

value

The value of the button toggle button.

TYPE: str | None

"},{"location":"components/button_toggle/#mesop.components.button_toggle.button_toggle.ButtonToggleChangeEvent","title":"ButtonToggleChangeEvent dataclass","text":"

Bases: MesopEvent

Event representing a change in the button toggle component's selected values.

ATTRIBUTE DESCRIPTION values

The new values of the button toggle component after the change.

TYPE: list[str]

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/button_toggle/#mesop.components.button_toggle.button_toggle.ButtonToggleChangeEvent.value","title":"value property","text":"

Shortcut for returning a single value.

"},{"location":"components/chat/","title":"Chat","text":""},{"location":"components/chat/#overview","title":"Overview","text":"

Chat component is a quick way to create a simple chat interface. Chat is part of Mesop Labs.

"},{"location":"components/chat/#examples","title":"Examples","text":"
import random\nimport time\n\nimport mesop as me\nimport mesop.labs as mel\n\n\ndef on_load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/chat\",\n  title=\"Mesop Demo Chat\",\n  on_load=on_load,\n)\ndef page():\n  mel.chat(transform, title=\"Mesop Demo Chat\", bot_user=\"Mesop Bot\")\n\n\ndef transform(input: str, history: list[mel.ChatMessage]):\n  for line in random.sample(LINES, random.randint(3, len(LINES) - 1)):\n    time.sleep(0.3)\n    yield line + \" \"\n\n\nLINES = [\n  \"Mesop is a Python-based UI framework designed to simplify web UI development for engineers without frontend experience.\",\n  \"It leverages the power of the Angular web framework and Angular Material components, allowing rapid construction of web demos and internal tools.\",\n  \"With Mesop, developers can enjoy a fast build-edit-refresh loop thanks to its hot reload feature, making UI tweaks and component integration seamless.\",\n  \"Deployment is straightforward, utilizing standard HTTP technologies.\",\n  \"Mesop's component library aims for comprehensive Angular Material component coverage, enhancing UI flexibility and composability.\",\n  \"It supports custom components for specific use cases, ensuring developers can extend its capabilities to fit their unique requirements.\",\n  \"Mesop's roadmap includes expanding its component library and simplifying the onboarding processs.\",\n]\n
"},{"location":"components/chat/#api","title":"API","text":""},{"location":"components/chat/#mesop.labs.chat.chat","title":"chat","text":"

Creates a simple chat UI which takes in a prompt and chat history and returns a response to the prompt.

This function creates event handlers for text input and output operations using the provided function transform to process the input and generate the output.

PARAMETER DESCRIPTION transform

Function that takes in a prompt and chat history and returns a response to the prompt.

TYPE: Callable[[str, list[ChatMessage]], Generator[str, None, None] | str]

title

Headline text to display at the top of the UI.

TYPE: str | None DEFAULT: None

bot_user

Name of your bot / assistant.

TYPE: str DEFAULT: _BOT_USER_DEFAULT

"},{"location":"components/checkbox/","title":"Checkbox","text":""},{"location":"components/checkbox/#overview","title":"Overview","text":"

Checkbox is a multi-selection form control and is based on the Angular Material checkbox component.

"},{"location":"components/checkbox/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  checked: bool\n\n\ndef on_update(event: me.CheckboxChangeEvent):\n  state = me.state(State)\n  state.checked = event.checked\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/checkbox\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    state = me.state(State)\n    me.checkbox(\n      \"Simple checkbox\",\n      on_change=on_update,\n    )\n\n    if state.checked:\n      me.text(text=\"is checked\")\n    else:\n      me.text(text=\"is not checked\")\n
"},{"location":"components/checkbox/#api","title":"API","text":""},{"location":"components/checkbox/#mesop.components.checkbox.checkbox.checkbox","title":"checkbox","text":"

Creates a simple Checkbox component with a text label.

PARAMETER DESCRIPTION label

Text label for checkbox

TYPE: str | None DEFAULT: None

on_change

Event emitted when the checkbox's checked value changes.

TYPE: Callable[[CheckboxChangeEvent], Any] | None DEFAULT: None

on_indeterminate_change

Event emitted when the checkbox's indeterminate value changes.

TYPE: Callable[[CheckboxIndeterminateChangeEvent], Any] | None DEFAULT: None

label_position

Whether the label should appear after or before the checkbox. Defaults to 'after'

TYPE: Literal['before', 'after'] DEFAULT: 'after'

disable_ripple

Whether the checkbox has a ripple.

TYPE: bool DEFAULT: False

tab_index

Tabindex for the checkbox.

TYPE: int DEFAULT: 0

color

Palette color of the checkbox.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

checked

Whether the checkbox is checked.

TYPE: bool DEFAULT: False

disabled

Whether the checkbox is disabled.

TYPE: bool DEFAULT: False

indeterminate

Whether the checkbox is indeterminate. This is also known as \"mixed\" mode and can be used to represent a checkbox with three states, e.g. a checkbox that represents a nested list of checkable items. Note that whenever checkbox is manually clicked, indeterminate is immediately set to false.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/checkbox/#mesop.components.checkbox.checkbox.content_checkbox","title":"content_checkbox","text":"

Creates a Checkbox component which is a composite component. Typically, you would use a text or icon component as a child.

Intended for advanced use cases.

PARAMETER DESCRIPTION on_change

Event emitted when the checkbox's checked value changes.

TYPE: Callable[[CheckboxChangeEvent], Any] | None DEFAULT: None

on_indeterminate_change

Event emitted when the checkbox's indeterminate value changes.

TYPE: Callable[[CheckboxIndeterminateChangeEvent], Any] | None DEFAULT: None

label_position

Whether the label should appear after or before the checkbox. Defaults to 'after'

TYPE: Literal['before', 'after'] DEFAULT: 'after'

disable_ripple

Whether the checkbox has a ripple.

TYPE: bool DEFAULT: False

tab_index

Tabindex for the checkbox.

TYPE: int DEFAULT: 0

color

Palette color of the checkbox.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

checked

Whether the checkbox is checked.

TYPE: bool DEFAULT: False

disabled

Whether the checkbox is disabled.

TYPE: bool DEFAULT: False

indeterminate

Whether the checkbox is indeterminate. This is also known as \"mixed\" mode and can be used to represent a checkbox with three states, e.g. a checkbox that represents a nested list of checkable items. Note that whenever checkbox is manually clicked, indeterminate is immediately set to false.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/checkbox/#mesop.components.checkbox.checkbox.CheckboxChangeEvent","title":"CheckboxChangeEvent dataclass","text":"

Bases: MesopEvent

Represents a checkbox state change event.

ATTRIBUTE DESCRIPTION checked

The new checked state of the checkbox.

TYPE: bool

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/checkbox/#mesop.components.checkbox.checkbox.CheckboxIndeterminateChangeEvent","title":"CheckboxIndeterminateChangeEvent dataclass","text":"

Bases: MesopEvent

Represents a checkbox indeterminate state change event.

ATTRIBUTE DESCRIPTION checked

The new indeterminate state of the checkbox.

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/code/","title":"Code","text":""},{"location":"components/code/#overview","title":"Overview","text":"

Code is used to render code with syntax highlighting. code is a simple wrapper around markdown.

"},{"location":"components/code/#examples","title":"Examples","text":"
import inspect\n\nimport mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/code_demo\",\n)\ndef code_demo():\n  with me.box(\n    style=me.Style(\n      padding=me.Padding.all(15),\n      background=me.theme_var(\"surface-container-lowest\"),\n    )\n  ):\n    me.text(\"Defaults to Python\")\n    me.code(\"a = 123\")\n\n    me.text(\"Can set to other languages\")\n    me.code(\"<div class='a'>foo</div>\", language=\"html\")\n\n    me.text(\"Bigger code block\")\n    me.code(inspect.getsource(me))\n
"},{"location":"components/code/#api","title":"API","text":""},{"location":"components/code/#mesop.components.code.code.code","title":"code","text":"

Creates a code component which displays code with syntax highlighting.

"},{"location":"components/date_picker/","title":"Date picker","text":""},{"location":"components/date_picker/#overview","title":"Overview","text":"

Date picker allows the user to enter free text or select a date from a calendar widget. and is based on the Angular Material datapicker component.

"},{"location":"components/date_picker/#examples","title":"Examples","text":"
from dataclasses import field\nfrom datetime import date\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  picked_date: date | None = field(default_factory=lambda: date(2024, 10, 1))\n\n\ndef on_load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(path=\"/date_picker\", on_load=on_load)\ndef app():\n  state = me.state(State)\n  with me.box(\n    style=me.Style(\n      display=\"flex\",\n      flex_direction=\"column\",\n      gap=15,\n      padding=me.Padding.all(15),\n    )\n  ):\n    me.date_picker(\n      label=\"Date\",\n      disabled=False,\n      placeholder=\"9/1/2024\",\n      required=True,\n      value=state.picked_date,\n      readonly=False,\n      hide_required_marker=False,\n      color=\"accent\",\n      float_label=\"always\",\n      appearance=\"outline\",\n      on_change=on_date_change,\n    )\n\n    me.text(\"Selected date: \" + _render_date(state.picked_date))\n\n\ndef on_date_change(e: me.DatePickerChangeEvent):\n  state = me.state(State)\n  state.picked_date = e.date\n\n\ndef _render_date(maybe_date: date | None) -> str:\n  if maybe_date:\n    return maybe_date.strftime(\"%Y-%m-%d\")\n  return \"None\"\n
"},{"location":"components/date_picker/#api","title":"API","text":""},{"location":"components/date_picker/#mesop.components.datepicker.datepicker.date_picker","title":"date_picker","text":"

Creates a date picker component.

PARAMETER DESCRIPTION label

Label for date picker input.

TYPE: str DEFAULT: ''

on_change

Fires when a valid date value has been specified through Calendar date selection or user input blur.

TYPE: Callable[[DatePickerChangeEvent], Any] | None DEFAULT: None

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

style

Style for date picker input.

TYPE: Style | None DEFAULT: None

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder

Placeholder value.

TYPE: str DEFAULT: ''

required

Whether it's required.

TYPE: bool DEFAULT: False

value

Initial value.

TYPE: date | None DEFAULT: None

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

hide_required_marker

Whether the required marker should be hidden.

TYPE: bool DEFAULT: False

color

The color palette for the form field.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

float_label

Whether the label should always float or float as the user types.

TYPE: Literal['always', 'auto'] DEFAULT: 'auto'

subscript_sizing

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

TYPE: Literal['fixed', 'dynamic'] DEFAULT: 'fixed'

hint_label

Text for the form field hint.

TYPE: str DEFAULT: ''

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/date_picker/#mesop.components.datepicker.datepicker.DatePickerChangeEvent","title":"DatePickerChangeEvent dataclass","text":"

Bases: MesopEvent

Represents a date picker change event.

This event will only fire if a valid date is specified.

ATTRIBUTE DESCRIPTION date

Date value

TYPE: date

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/date_range_picker/","title":"Date range picker","text":""},{"location":"components/date_range_picker/#overview","title":"Overview","text":"

Date range picker allows the user to enter free text or select a dates from a calendar widget. and is based on the Angular Material datapicker component.

"},{"location":"components/date_range_picker/#examples","title":"Examples","text":"
from dataclasses import field\nfrom datetime import date\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  picked_start_date: date | None = field(\n    default_factory=lambda: date(2024, 10, 1)\n  )\n  picked_end_date: date | None = field(\n    default_factory=lambda: date(2024, 11, 1)\n  )\n\n\ndef on_load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(path=\"/date_range_picker\", on_load=on_load)\ndef app():\n  state = me.state(State)\n  with me.box(\n    style=me.Style(\n      display=\"flex\",\n      flex_direction=\"column\",\n      gap=15,\n      padding=me.Padding.all(15),\n    )\n  ):\n    me.date_range_picker(\n      label=\"Date Range\",\n      disabled=False,\n      placeholder_start_date=\"9/1/2024\",\n      placeholder_end_date=\"10/1/2024\",\n      required=True,\n      start_date=state.picked_start_date,\n      end_date=state.picked_end_date,\n      readonly=False,\n      hide_required_marker=False,\n      color=\"accent\",\n      float_label=\"always\",\n      appearance=\"outline\",\n      on_change=on_date_range_change,\n    )\n\n    me.text(\"Start date: \" + _render_date(state.picked_start_date))\n    me.text(\"End date: \" + _render_date(state.picked_end_date))\n\n\ndef on_date_range_change(e: me.DateRangePickerChangeEvent):\n  state = me.state(State)\n  state.picked_start_date = e.start_date\n  state.picked_end_date = e.end_date\n\n\ndef _render_date(maybe_date: date | None) -> str:\n  if maybe_date:\n    return maybe_date.strftime(\"%Y-%m-%d\")\n  return \"None\"\n
"},{"location":"components/date_range_picker/#api","title":"API","text":""},{"location":"components/date_range_picker/#mesop.components.date_range_picker.date_range_picker.date_range_picker","title":"date_range_picker","text":"

Creates a date range picker component.

PARAMETER DESCRIPTION label

Label for date range picker input.

TYPE: str DEFAULT: ''

on_change

Fires when valid date values for both start/end have been specified through Calendar date selection or user input blur.

TYPE: Callable[[DateRangePickerChangeEvent], Any] | None DEFAULT: None

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

style

Style for date range picker input.

TYPE: Style | None DEFAULT: None

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder_start_date

Start date placeholder value.

TYPE: str DEFAULT: ''

placeholder_end_date

End date placeholder value.

TYPE: str DEFAULT: ''

required

Whether it's required.

TYPE: bool DEFAULT: False

start_date

Start date initial value.

TYPE: date | None DEFAULT: None

end_date

End date initial value.

TYPE: date | None DEFAULT: None

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

hide_required_marker

Whether the required marker should be hidden.

TYPE: bool DEFAULT: False

color

The color palette for the form field.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

float_label

Whether the label should always float or float as the user types.

TYPE: Literal['always', 'auto'] DEFAULT: 'auto'

subscript_sizing

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

TYPE: Literal['fixed', 'dynamic'] DEFAULT: 'fixed'

hint_label

Text for the form field hint.

TYPE: str DEFAULT: ''

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/date_range_picker/#mesop.components.date_range_picker.date_range_picker.DateRangePickerChangeEvent","title":"DateRangePickerChangeEvent dataclass","text":"

Bases: MesopEvent

Represents a date range picker change event.

This event will only fire if start and end dates are valid dates.

ATTRIBUTE DESCRIPTION start_date

Start date

TYPE: date

end_date

End date

TYPE: date

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/divider/","title":"Divider","text":""},{"location":"components/divider/#overview","title":"Overview","text":"

Divider is used to provide visual separation and is based on the Angular Material divider component.

"},{"location":"components/divider/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/divider\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(text=\"before\")\n    me.divider()\n    me.text(text=\"after\")\n
"},{"location":"components/divider/#api","title":"API","text":""},{"location":"components/divider/#mesop.components.divider.divider.divider","title":"divider","text":"

Creates a Divider component.

PARAMETER DESCRIPTION key

The component key.

TYPE: str | None DEFAULT: None

inset

Whether the divider is an inset divider.

TYPE: bool DEFAULT: False

"},{"location":"components/embed/","title":"Embed","text":""},{"location":"components/embed/#overview","title":"Overview","text":"

Embed allows you to embed/iframe another web site in your Mesop app.

"},{"location":"components/embed/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/embed\",\n)\ndef app():\n  src = \"https://google.github.io/mesop/\"\n  me.text(\"Embedding: \" + src, style=me.Style(padding=me.Padding.all(15)))\n  me.embed(\n    src=src,\n    style=me.Style(width=\"100%\", height=\"100%\"),\n  )\n
"},{"location":"components/embed/#api","title":"API","text":""},{"location":"components/embed/#mesop.components.embed.embed.embed","title":"embed","text":"

This function creates an embed component.

PARAMETER DESCRIPTION src

The source URL for the embed content.

TYPE: str

style

The style to apply to the embed, such as width and height.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/html/","title":"HTML","text":""},{"location":"components/html/#overview","title":"Overview","text":"

The HTML component allows you to add custom HTML to your Mesop app.

There are two modes for rendering HTML components:

  • sanitized (default), where the HTML is sanitized by Angular to remove potentially unsafe code like <script> and <style> for web security reasons.
  • sandboxed, which allows rendering of <script> and <style> HTML content by using an iframe sandbox.
"},{"location":"components/html/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/html_demo\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\"Sanitized HTML\", type=\"headline-5\")\n    me.html(\n      \"\"\"\n  Custom HTML\n  <a href=\"https://google.github.io/mesop/\" target=\"_blank\">mesop</a>\n  \"\"\",\n      mode=\"sanitized\",\n    )\n\n    with me.box(style=me.Style(margin=me.Margin.symmetric(vertical=24))):\n      me.divider()\n\n    me.text(\"Sandboxed HTML\", type=\"headline-5\")\n    me.html(\n      \"<style>body { color: #ff0000; }</style>hi<script>document.body.innerHTML = 'iamsandboxed'; </script>\",\n      mode=\"sandboxed\",\n    )\n
"},{"location":"components/html/#api","title":"API","text":""},{"location":"components/html/#mesop.components.html.html.html","title":"html","text":"

This function renders custom HTML in a secure way.

PARAMETER DESCRIPTION html

The HTML content to be rendered.

TYPE: str DEFAULT: ''

mode

Determines how the HTML is rendered. Mode can be either \"sanitized\" or \"sandboxed\". If \"sanitized\" then potentially dangerous content like <script> and <style> are stripped out. If \"sandboxed\", then all content is allowed, but rendered in an iframe for isolation.

TYPE: Literal['sanitized', 'sandboxed'] | None DEFAULT: None

style

The style to apply to the embed, such as width and height.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/icon/","title":"Icon","text":""},{"location":"components/icon/#overview","title":"Overview","text":"

Icon displays a Material icon/symbol and is based on the Angular Material icon component.

"},{"location":"components/icon/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/icon\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\"home icon\")\n    me.icon(icon=\"home\")\n
"},{"location":"components/icon/#api","title":"API","text":""},{"location":"components/icon/#mesop.components.icon.icon.icon","title":"icon","text":"

Creates a Icon component.

PARAMETER DESCRIPTION key

The component key.

TYPE: str | None DEFAULT: None

icon

Name of the Material Symbols icon.

TYPE: str | None DEFAULT: None

style

Inline styles

TYPE: Style | None DEFAULT: None

"},{"location":"components/image/","title":"Image","text":""},{"location":"components/image/#overview","title":"Overview","text":"

Image is the equivalent of an <img> HTML element.

"},{"location":"components/image/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/image\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.image(\n      src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-images/grapefruit-slice-332-332.jpg\",\n      alt=\"Grapefruit\",\n      style=me.Style(width=\"100%\"),\n    )\n
"},{"location":"components/image/#api","title":"API","text":""},{"location":"components/image/#mesop.components.image.image.image","title":"image","text":"

This function creates an image component.

PARAMETER DESCRIPTION src

The source URL of the image.

TYPE: str | None DEFAULT: None

alt

The alternative text for the image if it cannot be displayed.

TYPE: str | None DEFAULT: None

style

The style to apply to the image, such as width and height.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/input/","title":"Input","text":""},{"location":"components/input/#overview","title":"Overview","text":"

Input allows the user to type in a value and is based on the Angular Material input component.

For longer text inputs, also see Textarea

"},{"location":"components/input/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  input: str = \"\"\n\n\ndef on_blur(e: me.InputBlurEvent):\n  state = me.state(State)\n  state.input = e.value\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/input\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    s = me.state(State)\n    me.input(label=\"Basic input\", appearance=\"outline\", on_blur=on_blur)\n    me.text(text=s.input)\n
"},{"location":"components/input/#api","title":"API","text":""},{"location":"components/input/#mesop.components.input.input.input","title":"input","text":"

Creates a Input component.

PARAMETER DESCRIPTION label

Label for input.

TYPE: str DEFAULT: ''

on_blur

blur is fired when the input has lost focus.

TYPE: Callable[[InputBlurEvent], Any] | None DEFAULT: None

on_input

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

TYPE: Callable[[InputEvent], Any] | None DEFAULT: None

on_enter

triggers when the browser detects an \"Enter\" key on a keyup native browser event.

TYPE: Callable[[InputEnterEvent], Any] | None DEFAULT: None

type

Input type of the element. For textarea, use me.textarea(...)

TYPE: Literal['color', 'date', 'datetime-local', 'email', 'month', 'number', 'password', 'search', 'tel', 'text', 'time', 'url', 'week'] | None DEFAULT: None

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

style

Style for input.

TYPE: Style | None DEFAULT: None

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder

Placeholder value

TYPE: str DEFAULT: ''

required

Whether it's required

TYPE: bool DEFAULT: False

value

Initial value.

TYPE: str DEFAULT: ''

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

hide_required_marker

Whether the required marker should be hidden.

TYPE: bool DEFAULT: False

color

The color palette for the form field.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

float_label

Whether the label should always float or float as the user types.

TYPE: Literal['always', 'auto'] DEFAULT: 'auto'

subscript_sizing

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

TYPE: Literal['fixed', 'dynamic'] DEFAULT: 'fixed'

hint_label

Text for the form field hint.

TYPE: str DEFAULT: ''

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/input/#mesop.components.input.input.InputBlurEvent","title":"InputBlurEvent dataclass","text":"

Bases: MesopEvent

Represents an inpur blur event (when a user loses focus of an input).

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/input/#mesop.components.input.input.InputEnterEvent","title":"InputEnterEvent dataclass","text":"

Bases: MesopEvent

Represents an \"Enter\" keyboard event on an input component.

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/input/#mesop.events.InputEvent","title":"InputEvent dataclass","text":"

Bases: MesopEvent

Represents a user input event.

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/link/","title":"Link","text":""},{"location":"components/link/#overview","title":"Overview","text":"

Link creates an HTML anchor element (i.e. <a>) which links to another page.

"},{"location":"components/link/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/link\",\n)\ndef link():\n  with me.box(\n    style=me.Style(\n      margin=me.Margin.all(15), display=\"flex\", flex_direction=\"column\", gap=10\n    )\n  ):\n    me.link(\n      text=\"Open in same tab\",\n      url=\"https://google.github.io/mesop/\",\n      style=me.Style(color=me.theme_var(\"primary\")),\n    )\n    me.link(\n      text=\"Open in new tab\",\n      open_in_new_tab=True,\n      url=\"https://google.github.io/mesop/\",\n      style=me.Style(color=me.theme_var(\"primary\")),\n    )\n    me.link(\n      text=\"Styled link\",\n      url=\"https://google.github.io/mesop/\",\n      style=me.Style(color=me.theme_var(\"tertiary\"), text_decoration=\"none\"),\n    )\n
"},{"location":"components/link/#api","title":"API","text":""},{"location":"components/link/#mesop.components.link.link.link","title":"link","text":"

This function creates a link.

PARAMETER DESCRIPTION text

The text to be displayed.

TYPE: str

url

The URL to navigate to.

TYPE: str

open_in_new_tab

If True, open page in new tab. If False, open page in current tab.

TYPE: bool DEFAULT: False

style

Style for the component. Defaults to None.

TYPE: Style | None DEFAULT: None

key

Unique key for the component. Defaults to None.

TYPE: str | None DEFAULT: None

"},{"location":"components/markdown/","title":"Markdown","text":""},{"location":"components/markdown/#overview","title":"Overview","text":"

Markdown is used to render markdown text.

"},{"location":"components/markdown/#examples","title":"Examples","text":"
import mesop as me\n\nSAMPLE_MARKDOWN = \"\"\"\n# Sample Markdown Document\n\n## Table of Contents\n1. [Headers](#headers)\n2. [Emphasis](#emphasis)\n3. [Lists](#lists)\n4. [Links](#links)\n5. [Code](#code)\n6. [Blockquotes](#blockquotes)\n7. [Tables](#tables)\n8. [Horizontal Rules](#horizontal-rules)\n\n## Headers\n# Header 1\n## Header 2\n### Header 3\n#### Header 4\n##### Header 5\n###### Header 6\n\n## Emphasis\n*Italic text* or _Italic text_\n**Bold text** or __Bold text__\n***Bold and Italic*** or ___Bold and Italic___\n\n## Lists\n\n### Unordered List\n- Item 1\n- Item 2\n    - Subitem 2.1\n    - Subitem 2.2\n\n### Ordered List\n1. First item\n2. Second item\n    1. Subitem 2.1\n    2. Subitem 2.2\n\n## Links\n[Google](https://www.google.com/)\n\n## Inline Code\n\nInline `code`\n\n## Code\n\n```python\nimport mesop as me\n\n\n@me.page(path=\"/hello_world\")\ndef app():\n  me.text(\"Hello World\")\n
"},{"location":"components/markdown/#table","title":"Table","text":"First Header Second Header Content Cell Content Cell Content Cell Content Cell \"\"\"

def on_load(e: me.LoadEvent): me.set_theme_mode(\"system\")

@me.page( security_policy=me.SecurityPolicy( allowed_iframe_parents=[\"https://google.github.io\"] ), path=\"/markdown_demo\", on_load=on_load, ) def app(): with me.box( style=me.Style(background=me.theme_var(\"surface-container-lowest\")) ): me.markdown(SAMPLE_MARKDOWN, style=me.Style(margin=me.Margin.all(15))) ```

"},{"location":"components/markdown/#api","title":"API","text":""},{"location":"components/markdown/#mesop.components.markdown.markdown.markdown","title":"markdown","text":"

This function creates a markdown.

PARAMETER DESCRIPTION text

Required. Markdown text

TYPE: str | None DEFAULT: None

style

Style to apply to component. Follows HTML Element inline style API.

TYPE: Style | None DEFAULT: None

"},{"location":"components/plot/","title":"Plot","text":""},{"location":"components/plot/#overview","title":"Overview","text":"

Plot provides a convenient way to render Matplotlib figures as an image.

"},{"location":"components/plot/#examples","title":"Examples","text":"
from matplotlib.figure import Figure\n\nimport mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/plot\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    # Create matplotlib figure without using pyplot:\n    fig = Figure()\n    ax = fig.subplots()  # type: ignore\n    ax.plot([1, 2])  # type: ignore\n\n    me.text(\"Example using matplotlib:\", type=\"headline-5\")\n    me.plot(fig, style=me.Style(width=\"100%\"))\n
"},{"location":"components/plot/#api","title":"API","text":""},{"location":"components/plot/#mesop.components.plot.plot.plot","title":"plot","text":"

Creates a plot component from a Matplotlib figure.

PARAMETER DESCRIPTION figure

A Matplotlib figure which will be rendered.

TYPE: Figure

style

An optional Style object that defines the visual styling for the plot component. If None, default styling (e.g. height, width) is used.

TYPE: Style | None DEFAULT: None

"},{"location":"components/progress-bar/","title":"Progress bar","text":""},{"location":"components/progress-bar/#overview","title":"Overview","text":"

Progress Bar is used to indicate something is in progress and is based on the Angular Material progress bar component.

"},{"location":"components/progress-bar/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/progress_bar\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\"Default progress bar\", type=\"headline-5\")\n    me.progress_bar()\n
"},{"location":"components/progress-bar/#api","title":"API","text":""},{"location":"components/progress-bar/#mesop.components.progress_bar.progress_bar.progress_bar","title":"progress_bar","text":"

Creates a Progress bar component.

PARAMETER DESCRIPTION key

The component key.

TYPE: str | None DEFAULT: None

color

Theme palette color of the progress bar.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

value

Value of the progress bar. Defaults to zero. Mirrored to aria-valuenow.

TYPE: float DEFAULT: 0

buffer_value

Buffer value of the progress bar. Defaults to zero.

TYPE: float DEFAULT: 0

mode

Mode of the progress bar. Input must be one of these values: determinate, indeterminate, buffer, query, defaults to 'determinate'. Mirrored to mode attribute.

TYPE: Literal['determinate', 'indeterminate', 'buffer', 'query'] DEFAULT: 'indeterminate'

on_animation_end

Event emitted when animation of the primary progress bar completes. This event will not be emitted when animations are disabled, nor will it be emitted for modes with continuous animations (indeterminate and query).

TYPE: Callable[[ProgressBarAnimationEndEvent], Any] | None DEFAULT: None

"},{"location":"components/progress-bar/#mesop.components.progress_bar.progress_bar.ProgressBarAnimationEndEvent","title":"ProgressBarAnimationEndEvent dataclass","text":"

Bases: MesopEvent

Event emitted when the animation of the progress bar ends.

ATTRIBUTE DESCRIPTION value

The value of the progress bar when the animation ends.

TYPE: float

key

Key of the component that emitted this event.

TYPE: str

"},{"location":"components/progress-spinner/","title":"Progress spinner","text":""},{"location":"components/progress-spinner/#overview","title":"Overview","text":"

Progress Spinner is used to indicate something is in progress and is based on the Angular Material progress spinner component.

"},{"location":"components/progress-spinner/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/progress_spinner\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.progress_spinner()\n
"},{"location":"components/progress-spinner/#api","title":"API","text":""},{"location":"components/progress-spinner/#mesop.components.progress_spinner.progress_spinner.progress_spinner","title":"progress_spinner","text":"

Creates a Progress spinner component.

PARAMETER DESCRIPTION key

The component key.

TYPE: str | None DEFAULT: None

color

Theme palette color of the progress spinner.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

diameter

The diameter of the progress spinner (will set width and height of svg).

TYPE: float DEFAULT: 48

stroke_width

Stroke width of the progress spinner.

TYPE: float DEFAULT: 4

"},{"location":"components/radio/","title":"Radio","text":""},{"location":"components/radio/#overview","title":"Overview","text":"

Radio is a single selection form control based on the Angular Material radio component.

"},{"location":"components/radio/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  radio_value: str = \"2\"\n\n\ndef on_change(event: me.RadioChangeEvent):\n  s = me.state(State)\n  s.radio_value = event.value\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/radio\",\n)\ndef app():\n  s = me.state(State)\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\"Horizontal radio options\")\n    me.radio(\n      on_change=on_change,\n      options=[\n        me.RadioOption(label=\"Option 1\", value=\"1\"),\n        me.RadioOption(label=\"Option 2\", value=\"2\"),\n      ],\n      value=s.radio_value,\n    )\n    me.text(text=\"Selected radio value: \" + s.radio_value)\n
"},{"location":"components/radio/#api","title":"API","text":""},{"location":"components/radio/#mesop.components.radio.radio.radio","title":"radio","text":"

Creates a Radio component.

PARAMETER DESCRIPTION options

List of radio options

TYPE: Iterable[RadioOption] DEFAULT: ()

on_change

Event emitted when the group value changes. Change events are only emitted when the value changes due to user interaction with a radio button (the same behavior as <input type-\"radio\">).

TYPE: Callable[[RadioChangeEvent], Any] | None DEFAULT: None

color

Theme color for all of the radio buttons in the group.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

label_position

Whether the labels should appear after or before the radio-buttons. Defaults to 'after'

TYPE: Literal['before', 'after'] DEFAULT: 'after'

value

Value for the radio-group. Should equal the value of the selected radio button if there is a corresponding radio button with a matching value.

TYPE: str DEFAULT: ''

disabled

Whether the radio group is disabled.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/radio/#mesop.components.radio.radio.RadioOption","title":"RadioOption dataclass","text":"ATTRIBUTE DESCRIPTION label

Content to show for the radio option

TYPE: str | None

value

The value of this radio button.

TYPE: str | None

"},{"location":"components/radio/#mesop.components.radio.radio.RadioChangeEvent","title":"RadioChangeEvent dataclass","text":"

Bases: MesopEvent

Event representing a change in the radio component's value.

ATTRIBUTE DESCRIPTION value

The new value of the radio component after the change.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/select/","title":"Select","text":""},{"location":"components/select/#overview","title":"Overview","text":"

Select allows the user to choose from a list of values and is based on the Angular Material select component.

"},{"location":"components/select/#examples","title":"Examples","text":"
from dataclasses import field\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  selected_values: list[str] = field(\n    default_factory=lambda: [\"value1\", \"value3\"]\n  )\n\n\ndef on_selection_change(e: me.SelectSelectionChangeEvent):\n  s = me.state(State)\n  s.selected_values = e.values\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/select_demo\",\n)\ndef app():\n  state = me.state(State)\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.select(\n      label=\"Select\",\n      options=[\n        me.SelectOption(label=\"label 1\", value=\"value1\"),\n        me.SelectOption(label=\"label 2\", value=\"value2\"),\n        me.SelectOption(label=\"label 3\", value=\"value3\"),\n      ],\n      on_selection_change=on_selection_change,\n      style=me.Style(width=500),\n      multiple=True,\n      appearance=\"outline\",\n      value=state.selected_values,\n    )\n    me.text(text=\"Selected values: \" + \", \".join(state.selected_values))\n
"},{"location":"components/select/#api","title":"API","text":""},{"location":"components/select/#mesop.components.select.select.select","title":"select","text":"

Creates a Select component.

PARAMETER DESCRIPTION options

List of select options.

TYPE: Iterable[SelectOption] DEFAULT: ()

on_selection_change

Event emitted when the selected value has been changed by the user.

TYPE: Callable[[SelectSelectionChangeEvent], Any] | None DEFAULT: None

on_opened_change

Event emitted when the select panel has been toggled.

TYPE: Callable[[SelectOpenedChangeEvent], Any] | None DEFAULT: None

disabled

Whether the select is disabled.

TYPE: bool DEFAULT: False

disable_ripple

Whether ripples in the select are disabled.

TYPE: bool DEFAULT: False

multiple

Whether multiple selections are allowed.

TYPE: bool DEFAULT: False

tab_index

Tab index of the select.

TYPE: int DEFAULT: 0

placeholder

Placeholder to be shown if no value has been selected.

TYPE: str DEFAULT: ''

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

value

Value(s) of the select control.

TYPE: list[str] | str DEFAULT: ''

style

Style.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/select/#mesop.components.select.select.SelectOption","title":"SelectOption dataclass","text":"

Represents an option within a select component.

ATTRIBUTE DESCRIPTION label

The content shown for the select option.

TYPE: str | None

value

The value associated with the select option.

TYPE: str | None

"},{"location":"components/select/#mesop.components.select.select.SelectSelectionChangeEvent","title":"SelectSelectionChangeEvent dataclass","text":"

Bases: MesopEvent

Event representing a change in the select component's value(s).

ATTRIBUTE DESCRIPTION values

New values of the select component after the change.

TYPE: list[str]

key

Key of the component that emitted this event.

TYPE: str

"},{"location":"components/select/#mesop.components.select.select.SelectSelectionChangeEvent.value","title":"value property","text":"

Shortcut for returning a single value.

"},{"location":"components/select/#mesop.components.select.select.SelectOpenedChangeEvent","title":"SelectOpenedChangeEvent dataclass","text":"

Bases: MesopEvent

Event representing the opened state change of the select component.

ATTRIBUTE DESCRIPTION opened

A boolean indicating whether the select component is opened (True) or closed (False).

TYPE: bool

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/sidenav/","title":"Sidenav","text":""},{"location":"components/sidenav/#overview","title":"Overview","text":"

Sidenav is a sidebar typically used for navigation and is based on the Angular Material sidenav component.

"},{"location":"components/sidenav/#examples","title":"Examples","text":"
import mesop as me\n\nSIDENAV_WIDTH = 200\n\n\n@me.stateclass\nclass State:\n  sidenav_open: bool\n\n\ndef on_click(e: me.ClickEvent):\n  s = me.state(State)\n  s.sidenav_open = not s.sidenav_open\n\n\ndef opened_changed(e: me.SidenavOpenedChangedEvent):\n  s = me.state(State)\n  s.sidenav_open = e.opened\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/sidenav\",\n)\ndef app():\n  state = me.state(State)\n  with me.sidenav(\n    opened=state.sidenav_open,\n    disable_close=False,\n    on_opened_changed=opened_changed,\n    style=me.Style(\n      border_radius=0,\n      width=SIDENAV_WIDTH,\n      background=me.theme_var(\"surface-container-low\"),\n      padding=me.Padding.all(15),\n    ),\n  ):\n    me.text(\"Inside sidenav\")\n\n  with me.box(\n    style=me.Style(\n      margin=me.Margin(left=SIDENAV_WIDTH if state.sidenav_open else 0),\n      padding=me.Padding.all(15),\n    ),\n  ):\n    with me.content_button(on_click=on_click):\n      me.icon(\"menu\")\n    me.markdown(\"Main content\")\n
"},{"location":"components/sidenav/#api","title":"API","text":""},{"location":"components/sidenav/#mesop.components.sidenav.sidenav.sidenav","title":"sidenav","text":"

This function creates a sidenav.

PARAMETER DESCRIPTION opened

A flag to determine if the sidenav is open or closed. Defaults to True.

TYPE: bool DEFAULT: True

disable_close

Whether the drawer can be closed with the escape key.

TYPE: bool DEFAULT: True

position

The side that the drawer is attached to.

TYPE: Literal['start', 'end'] DEFAULT: 'start'

on_opened_changed

Handles event emitted when the drawer open state is changed.

TYPE: Callable[[SidenavOpenedChangedEvent], Any] | None DEFAULT: None

style

An optional Style object to apply custom styles. Defaults to None.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/slide-toggle/","title":"Slide toggle","text":""},{"location":"components/slide-toggle/#overview","title":"Overview","text":"

Slide Toggle allows the user to toggle on and off and is based on the Angular Material slide toggle component.

"},{"location":"components/slide-toggle/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  toggled: bool = False\n\n\ndef on_change(event: me.SlideToggleChangeEvent):\n  s = me.state(State)\n  s.toggled = not s.toggled\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/slide_toggle\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.slide_toggle(label=\"Slide toggle\", on_change=on_change)\n    s = me.state(State)\n    me.text(text=f\"Toggled: {s.toggled}\")\n
"},{"location":"components/slide-toggle/#api","title":"API","text":""},{"location":"components/slide-toggle/#mesop.components.slide_toggle.slide_toggle.slide_toggle","title":"slide_toggle","text":"

Creates a simple Slide toggle component with a text label.

PARAMETER DESCRIPTION label

Text label for slide toggle

TYPE: str | None DEFAULT: None

on_change

An event will be dispatched each time the slide-toggle changes its value.

TYPE: Callable[[SlideToggleChangeEvent], Any] | None DEFAULT: None

label_position

Whether the label should appear after or before the slide-toggle. Defaults to 'after'.

TYPE: Literal['before', 'after'] DEFAULT: 'after'

required

Whether the slide-toggle is required.

TYPE: bool DEFAULT: False

color

Palette color of slide toggle.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

disabled

Whether the slide toggle is disabled.

TYPE: bool DEFAULT: False

disable_ripple

Whether the slide toggle has a ripple.

TYPE: bool DEFAULT: False

tab_index

Tabindex of slide toggle.

TYPE: int DEFAULT: 0

checked

Whether the slide-toggle element is checked or not.

TYPE: bool DEFAULT: False

hide_icon

Whether to hide the icon inside of the slide toggle.

TYPE: bool DEFAULT: False

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/slide-toggle/#mesop.components.slide_toggle.slide_toggle.content_slide_toggle","title":"content_slide_toggle","text":"

Creates a Slide toggle component which is a composite component. Typically, you would use a text or icon component as a child.

Intended for advanced use cases.

PARAMETER DESCRIPTION on_change

An event will be dispatched each time the slide-toggle changes its value.

TYPE: Callable[[SlideToggleChangeEvent], Any] | None DEFAULT: None

label_position

Whether the label should appear after or before the slide-toggle. Defaults to 'after'.

TYPE: Literal['before', 'after'] DEFAULT: 'after'

required

Whether the slide-toggle is required.

TYPE: bool DEFAULT: False

color

Palette color of slide toggle.

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

disabled

Whether the slide toggle is disabled.

TYPE: bool DEFAULT: False

disable_ripple

Whether the slide toggle has a ripple.

TYPE: bool DEFAULT: False

tab_index

Tabindex of slide toggle.

TYPE: int DEFAULT: 0

checked

Whether the slide-toggle element is checked or not.

TYPE: bool DEFAULT: False

hide_icon

Whether to hide the icon inside of the slide toggle.

TYPE: bool DEFAULT: False

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/slide-toggle/#mesop.components.slide_toggle.slide_toggle.SlideToggleChangeEvent","title":"SlideToggleChangeEvent dataclass","text":"

Bases: MesopEvent

Event triggered when the slide toggle state changes.

ATTRIBUTE DESCRIPTION key

Key of the component that emitted this event.

TYPE: str

"},{"location":"components/slider/","title":"Slider","text":""},{"location":"components/slider/#overview","title":"Overview","text":"

Slider allows the user to select from a range of values and is based on the Angular Material slider component.

"},{"location":"components/slider/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  initial_input_value: str = \"50.0\"\n  initial_slider_value: float = 50.0\n  slider_value: float = 50.0\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/slider\",\n)\ndef app():\n  state = me.state(State)\n  with me.box(\n    style=me.Style(\n      display=\"flex\", flex_direction=\"column\", margin=me.Margin.all(15)\n    )\n  ):\n    me.input(\n      label=\"Slider value\",\n      appearance=\"outline\",\n      value=state.initial_input_value,\n      on_input=on_input,\n    )\n    me.slider(on_value_change=on_value_change, value=state.initial_slider_value)\n    me.text(text=f\"Value: {me.state(State).slider_value}\")\n\n\ndef on_value_change(event: me.SliderValueChangeEvent):\n  state = me.state(State)\n  state.slider_value = event.value\n  state.initial_input_value = str(state.slider_value)\n\n\ndef on_input(event: me.InputEvent):\n  state = me.state(State)\n  state.initial_slider_value = float(event.value)\n  state.slider_value = state.initial_slider_value\n
"},{"location":"components/slider/#api","title":"API","text":""},{"location":"components/slider/#mesop.components.slider.slider.slider","title":"slider","text":"

Creates a Slider component.

PARAMETER DESCRIPTION on_value_change

An event will be dispatched each time the slider changes its value.

TYPE: Callable[[SliderValueChangeEvent], Any] | None DEFAULT: None

value

Initial value. If updated, the slider will be updated with a new initial value.

TYPE: float | None DEFAULT: None

min

The minimum value that the slider can have.

TYPE: float DEFAULT: 0

max

The maximum value that the slider can have.

TYPE: float DEFAULT: 100

step

The values at which the thumb will snap.

TYPE: float DEFAULT: 1

disabled

Whether the slider is disabled.

TYPE: bool DEFAULT: False

discrete

Whether the slider displays a numeric value label upon pressing the thumb.

TYPE: bool DEFAULT: False

show_tick_marks

Whether the slider displays tick marks along the slider track.

TYPE: bool DEFAULT: False

color

Palette color of the slider.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

disable_ripple

Whether ripples are disabled in the slider.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/slider/#mesop.components.slider.slider.SliderValueChangeEvent","title":"SliderValueChangeEvent dataclass","text":"

Bases: MesopEvent

Event triggered when the slider value changes.

ATTRIBUTE DESCRIPTION value

The new value of the slider after the change.

TYPE: float

key

Key of the component that emitted this event.

TYPE: str

"},{"location":"components/table/","title":"Table","text":""},{"location":"components/table/#overview","title":"Overview","text":"

Table allows the user to render an Angular Material table component from a Pandas data frame.

"},{"location":"components/table/#examples","title":"Examples","text":"
from datetime import datetime\n\nimport numpy as np\nimport pandas as pd\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  selected_cell: str = \"No cell selected.\"\n\n\ndf = pd.DataFrame(\n  data={\n    \"NA\": [pd.NA, pd.NA, pd.NA],\n    \"Index\": [3, 2, 1],\n    \"Bools\": [True, False, np.bool_(True)],\n    \"Ints\": [101, 90, np.int64(-55)],\n    \"Floats\": [2.3, 4.5, np.float64(-3.000000003)],\n    \"Strings\": [\"Hello\", \"World\", \"!\"],\n    \"Date Times\": [\n      pd.Timestamp(\"20180310\"),\n      pd.Timestamp(\"20230310\"),\n      datetime(2023, 1, 1, 12, 12, 1),\n    ],\n  }\n)\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/table\",\n)\ndef app():\n  state = me.state(State)\n\n  with me.box(style=me.Style(padding=me.Padding.all(10), width=500)):\n    me.table(\n      df,\n      on_click=on_click,\n      header=me.TableHeader(sticky=True),\n      columns={\n        \"NA\": me.TableColumn(sticky=True),\n        \"Index\": me.TableColumn(sticky=True),\n      },\n    )\n\n  with me.box(\n    style=me.Style(\n      background=me.theme_var(\"surface-container-high\"),\n      margin=me.Margin.all(10),\n      padding=me.Padding.all(10),\n    )\n  ):\n    me.text(state.selected_cell)\n\n\ndef on_click(e: me.TableClickEvent):\n  state = me.state(State)\n  state.selected_cell = (\n    f\"Selected cell at col {e.col_index} and row {e.row_index} \"\n    f\"with value {df.iat[e.row_index, e.col_index]!s}\"\n  )\n
"},{"location":"components/table/#api","title":"API","text":""},{"location":"components/table/#mesop.components.table.table.table","title":"table","text":"

This function creates a table from Pandas data frame

PARAMETER DESCRIPTION data_frame

Pandas data frame.

TYPE: Any

on_click

Triggered when a table cell is clicked. The click event is a native browser event.

TYPE: Callable[[TableClickEvent], Any] | None DEFAULT: None

header

Configures table header to be sticky or not.

TYPE: TableHeader | None DEFAULT: None

columns

Configures table columns to be sticky or not. The key is the name of the column.

TYPE: dict[str, TableColumn] | None DEFAULT: None

"},{"location":"components/text-to-image/","title":"Text to Image","text":""},{"location":"components/text-to-image/#overview","title":"Overview","text":"

Text To Image component is a quick and simple way of getting started with Mesop. Text To Image is part of Mesop Labs.

"},{"location":"components/text-to-image/#examples","title":"Examples","text":"
import mesop as me\nimport mesop.labs as mel\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/text_to_image\",\n  title=\"Text to Image Example\",\n)\ndef app():\n  mel.text_to_image(\n    generate_image,\n    title=\"Text to Image Example\",\n  )\n\n\ndef generate_image(prompt: str):\n  return \"https://www.google.com/logos/doodles/2024/earth-day-2024-6753651837110453-2xa.gif\"\n
"},{"location":"components/text-to-image/#api","title":"API","text":""},{"location":"components/text-to-image/#mesop.labs.text_to_image.text_to_image","title":"text_to_image","text":"

Creates a simple UI which takes in a text input and returns an image output.

This function creates event handlers for text input and output operations using the provided function transform to process the input and generate the image output.

PARAMETER DESCRIPTION transform

Function that takes in a string input and returns a URL to an image or a base64 encoded image.

TYPE: Callable[[str], str]

title

Headline text to display at the top of the UI.

TYPE: str | None DEFAULT: None

"},{"location":"components/text-to-text/","title":"Text to Text","text":""},{"location":"components/text-to-text/#overview","title":"Overview","text":"

Text to text component allows you to take in user inputted text and return a transformed text. This is part of Mesop Labs.

"},{"location":"components/text-to-text/#examples","title":"Examples","text":"
import mesop as me\nimport mesop.labs as mel\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/text_to_text\",\n  title=\"Text to Text Example\",\n)\ndef app():\n  mel.text_to_text(\n    upper_case_stream,\n    title=\"Text to Text Example\",\n  )\n\n\ndef upper_case_stream(s: str):\n  return \"Echo: \" + s\n
"},{"location":"components/text-to-text/#api","title":"API","text":""},{"location":"components/text-to-text/#mesop.labs.text_to_text.text_to_text","title":"text_to_text","text":"

Creates a simple UI which takes in a text input and returns a text output.

This function creates event handlers for text input and output operations using the provided transform function to process the input and generate the output.

PARAMETER DESCRIPTION transform

Function that takes in a string input and either returns or yields a string output.

TYPE: Callable[[str], Generator[str, None, None] | str]

title

Headline text to display at the top of the UI

TYPE: str | None DEFAULT: None

transform_mode

Specifies how the output should be updated when yielding an output using a generator. - \"append\": Concatenates each new piece of text to the existing output. - \"replace\": Replaces the existing output with each new piece of text.

TYPE: Literal['append', 'replace'] DEFAULT: 'append'

"},{"location":"components/text-to-text/#mesop.labs.text_to_text.text_io","title":"text_io","text":"

Deprecated: Use text_to_text instead which provides the same functionality with better default settings.

This function creates event handlers for text input and output operations using the provided transform function to process the input and generate the output.

PARAMETER DESCRIPTION transform

Function that takes in a string input and either returns or yields a string output.

TYPE: Callable[[str], Generator[str, None, None] | str]

title

Headline text to display at the top of the UI

TYPE: str | None DEFAULT: None

transform_mode

Specifies how the output should be updated when yielding an output using a generator. - \"append\": Concatenates each new piece of text to the existing output. - \"replace\": Replaces the existing output with each new piece of text.

TYPE: Literal['append', 'replace'] DEFAULT: 'replace'

"},{"location":"components/text/","title":"Text","text":""},{"location":"components/text/#overview","title":"Overview","text":"

Text displays text as-is. If you have markdown text, use the Markdown component.

"},{"location":"components/text/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/text\",\n)\ndef text():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(text=\"headline-1: Hello, world!\", type=\"headline-1\")\n    me.text(text=\"headline-2: Hello, world!\", type=\"headline-2\")\n    me.text(text=\"headline-3: Hello, world!\", type=\"headline-3\")\n    me.text(text=\"headline-4: Hello, world!\", type=\"headline-4\")\n    me.text(text=\"headline-5: Hello, world!\", type=\"headline-5\")\n    me.text(text=\"headline-6: Hello, world!\", type=\"headline-6\")\n    me.text(text=\"subtitle-1: Hello, world!\", type=\"subtitle-1\")\n    me.text(text=\"subtitle-2: Hello, world!\", type=\"subtitle-2\")\n    me.text(text=\"body-1: Hello, world!\", type=\"body-1\")\n    me.text(text=\"body-2: Hello, world!\", type=\"body-2\")\n    me.text(text=\"caption: Hello, world!\", type=\"caption\")\n    me.text(text=\"button: Hello, world!\", type=\"button\")\n
"},{"location":"components/text/#api","title":"API","text":""},{"location":"components/text/#mesop.components.text.text.text","title":"text","text":"

Create a text component.

PARAMETER DESCRIPTION text

The text to display.

TYPE: str | None DEFAULT: None

type

The typography level for the text.

TYPE: Literal['headline-1', 'headline-2', 'headline-3', 'headline-4', 'headline-5', 'headline-6', 'subtitle-1', 'subtitle-2', 'body-1', 'body-2', 'caption', 'button'] | None DEFAULT: None

style

Style to apply to component. Follows HTML Element inline style API.

TYPE: Style | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/textarea/","title":"Textarea","text":""},{"location":"components/textarea/#overview","title":"Overview","text":"

Textarea allows the user to type in a value and is based on the Angular Material input component for <textarea>.

This is similar to Input, but Textarea is better suited for long text inputs.

"},{"location":"components/textarea/#examples","title":"Examples","text":"
import mesop as me\n\n\n@me.stateclass\nclass State:\n  input: str = \"\"\n  output: str = \"\"\n\n\ndef on_blur(e: me.InputBlurEvent):\n  state = me.state(State)\n  state.input = e.value\n  state.output = e.value\n\n\ndef on_newline(e: me.TextareaShortcutEvent):\n  state = me.state(State)\n  state.input = e.value + \"\\n\"\n\n\ndef on_submit(e: me.TextareaShortcutEvent):\n  state = me.state(State)\n  state.input = e.value\n  state.output = e.value\n\n\ndef on_clear(e: me.TextareaShortcutEvent):\n  state = me.state(State)\n  state.input = \"\"\n  state.output = \"\"\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/textarea\",\n)\ndef app():\n  s = me.state(State)\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.text(\n      \"Press enter to submit.\",\n      style=me.Style(margin=me.Margin(bottom=15)),\n    )\n    me.text(\n      \"Press shift+enter to create new line.\",\n      style=me.Style(margin=me.Margin(bottom=15)),\n    )\n    me.text(\n      \"Press shift+meta+enter to clear text.\",\n      style=me.Style(margin=me.Margin(bottom=15)),\n    )\n    me.textarea(\n      label=\"Basic input\",\n      value=s.input,\n      on_blur=on_blur,\n      shortcuts={\n        me.Shortcut(key=\"enter\"): on_submit,\n        me.Shortcut(shift=True, key=\"ENTER\"): on_newline,\n        me.Shortcut(shift=True, meta=True, key=\"Enter\"): on_clear,\n      },\n      appearance=\"outline\",\n      style=me.Style(width=\"100%\"),\n    )\n    me.text(text=s.output)\n
"},{"location":"components/textarea/#api","title":"API","text":""},{"location":"components/textarea/#mesop.components.input.input.textarea","title":"textarea","text":"

Creates a Textarea component.

PARAMETER DESCRIPTION label

Label for input.

TYPE: str DEFAULT: ''

on_blur

blur is fired when the input has lost focus.

TYPE: Callable[[InputBlurEvent], Any] | None DEFAULT: None

on_input

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

TYPE: Callable[[InputEvent], Any] | None DEFAULT: None

autosize

If True, the textarea will automatically adjust its height to fit the content, up to the max_rows limit.

TYPE: bool DEFAULT: False

min_rows

The minimum number of rows the textarea will display.

TYPE: int | None DEFAULT: None

max_rows

The maximum number of rows the textarea will display.

TYPE: int | None DEFAULT: None

rows

The number of lines to show in the text area.

TYPE: int DEFAULT: 5

appearance

The form field appearance style.

TYPE: Literal['fill', 'outline'] DEFAULT: 'fill'

style

Style for input.

TYPE: Style | None DEFAULT: None

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder

Placeholder value

TYPE: str DEFAULT: ''

required

Whether it's required

TYPE: bool DEFAULT: False

value

Initial value.

TYPE: str DEFAULT: ''

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

hide_required_marker

Whether the required marker should be hidden.

TYPE: bool DEFAULT: False

color

The color palette for the form field.

TYPE: Literal['primary', 'accent', 'warn'] DEFAULT: 'primary'

float_label

Whether the label should always float or float as the user types.

TYPE: Literal['always', 'auto'] DEFAULT: 'auto'

subscript_sizing

Whether the form field should reserve space for one line of hint/error text (default) or to have the spacing grow from 0px as needed based on the size of the hint/error content. Note that when using dynamic sizing, layout shifts will occur when hint/error text changes.

TYPE: Literal['fixed', 'dynamic'] DEFAULT: 'fixed'

hint_label

Text for the form field hint.

TYPE: str DEFAULT: ''

shortcuts

Shortcut events to listen for.

TYPE: dict[Shortcut, Callable[[TextareaShortcutEvent], Any]] | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/textarea/#mesop.components.input.input.native_textarea","title":"native_textarea","text":"

Creates a browser native Textarea component. Intended for advanced use cases with maximum UI control.

PARAMETER DESCRIPTION on_blur

blur is fired when the input has lost focus.

TYPE: Callable[[InputBlurEvent], Any] | None DEFAULT: None

on_input

input is fired whenever the input has changed (e.g. user types). Note: this can cause performance issues. Use on_blur instead.

TYPE: Callable[[InputEvent], Any] | None DEFAULT: None

autosize

If True, the textarea will automatically adjust its height to fit the content, up to the max_rows limit.

TYPE: bool DEFAULT: False

min_rows

The minimum number of rows the textarea will display.

TYPE: int | None DEFAULT: None

max_rows

The maximum number of rows the textarea will display.

TYPE: int | None DEFAULT: None

style

Style for input.

TYPE: Style | None DEFAULT: None

disabled

Whether it's disabled.

TYPE: bool DEFAULT: False

placeholder

Placeholder value

TYPE: str DEFAULT: ''

value

Initial value.

TYPE: str DEFAULT: ''

readonly

Whether the element is readonly.

TYPE: bool DEFAULT: False

shortcuts

Shortcut events to listen for.

TYPE: dict[Shortcut, Callable[[TextareaShortcutEvent], Any]] | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

"},{"location":"components/textarea/#mesop.components.input.input.InputBlurEvent","title":"InputBlurEvent dataclass","text":"

Bases: MesopEvent

Represents an inpur blur event (when a user loses focus of an input).

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/textarea/#mesop.events.InputEvent","title":"InputEvent dataclass","text":"

Bases: MesopEvent

Represents a user input event.

ATTRIBUTE DESCRIPTION value

Input value.

TYPE: str

key

key of the component that emitted this event.

TYPE: str

"},{"location":"components/tooltip/","title":"Tooltip","text":""},{"location":"components/tooltip/#overview","title":"Overview","text":"

Tooltip is based on the Angular Material tooltip component.

"},{"location":"components/tooltip/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/tooltip\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    with me.tooltip(message=\"Tooltip message\"):\n      me.text(text=\"Hello, World\")\n
"},{"location":"components/tooltip/#api","title":"API","text":""},{"location":"components/tooltip/#mesop.components.tooltip.tooltip.tooltip","title":"tooltip","text":"

Creates a Tooltip component. Tooltip is a composite component.

PARAMETER DESCRIPTION key

The component key.

TYPE: str | None DEFAULT: None

position

Allows the user to define the position of the tooltip relative to the parent element

TYPE: Literal['left', 'right', 'above', 'below', 'before', 'after'] DEFAULT: 'left'

position_at_origin

Whether tooltip should be relative to the click or touch origin instead of outside the element bounding box.

TYPE: bool DEFAULT: False

disabled

Disables the display of the tooltip.

TYPE: bool DEFAULT: False

show_delay_ms

The default delay in ms before showing the tooltip after show is called

TYPE: int DEFAULT: 0

hide_delay_ms

The default delay in ms before hiding the tooltip after hide is called

TYPE: int DEFAULT: 0

message

The message to be displayed in the tooltip

TYPE: str DEFAULT: ''

"},{"location":"components/uploader/","title":"Uploader","text":""},{"location":"components/uploader/#overview","title":"Overview","text":"

Uploader is the equivalent of an <input type=\"file> HTML element except it uses a custom UI that better matches the look of Angular Material Components.

"},{"location":"components/uploader/#examples","title":"Examples","text":"
import base64\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  file: me.UploadedFile\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/uploader\",\n)\ndef app():\n  state = me.state(State)\n  with me.box(style=me.Style(padding=me.Padding.all(15))):\n    me.uploader(\n      label=\"Upload Image\",\n      accepted_file_types=[\"image/jpeg\", \"image/png\"],\n      on_upload=handle_upload,\n      type=\"flat\",\n      color=\"primary\",\n      style=me.Style(font_weight=\"bold\"),\n    )\n\n    if state.file.size:\n      with me.box(style=me.Style(margin=me.Margin.all(10))):\n        me.text(f\"File name: {state.file.name}\")\n        me.text(f\"File size: {state.file.size}\")\n        me.text(f\"File type: {state.file.mime_type}\")\n\n      with me.box(style=me.Style(margin=me.Margin.all(10))):\n        me.image(src=_convert_contents_data_url(state.file))\n\n\ndef handle_upload(event: me.UploadEvent):\n  state = me.state(State)\n  state.file = event.file\n\n\ndef _convert_contents_data_url(file: me.UploadedFile) -> str:\n  return (\n    f\"data:{file.mime_type};base64,{base64.b64encode(file.getvalue()).decode()}\"\n  )\n
"},{"location":"components/uploader/#api","title":"API","text":""},{"location":"components/uploader/#mesop.components.uploader.uploader.uploader","title":"uploader","text":"

This function creates an uploader.

PARAMETER DESCRIPTION label

Upload button label.

TYPE: str

accepted_file_types

List of accepted file types. See the accept parameter.

TYPE: Sequence[str] | None DEFAULT: None

key

The component key.

TYPE: str | None DEFAULT: None

on_upload

File upload event handler.

TYPE: Callable[[UploadEvent], Any] | None DEFAULT: None

type

Type of button style to use

TYPE: Literal['raised', 'flat', 'stroked'] | None DEFAULT: None

color

Theme color palette of the button

TYPE: Literal['primary', 'accent', 'warn'] | None DEFAULT: None

disable_ripple

Whether the ripple effect is disabled or not.

TYPE: bool DEFAULT: False

disabled

Whether the button is disabled.

TYPE: bool DEFAULT: False

style

Style for the component.

TYPE: Style | None DEFAULT: None

"},{"location":"components/uploader/#mesop.components.uploader.uploader.UploadEvent","title":"UploadEvent dataclass","text":"

Bases: MesopEvent

Event for file uploads.

ATTRIBUTE DESCRIPTION file

Uploaded file.

TYPE: UploadedFile

"},{"location":"components/uploader/#mesop.components.uploader.uploader.UploadedFile","title":"UploadedFile","text":"

Bases: BytesIO

Uploaded file contents and metadata.

"},{"location":"components/video/","title":"Video","text":""},{"location":"components/video/#overview","title":"Overview","text":"

Video is the equivalent of an <video> HTML element. Video displays the browser's native video controls.

"},{"location":"components/video/#examples","title":"Examples","text":"
import mesop as me\n\n\ndef load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n\n\n@me.page(\n  on_load=load,\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.github.io\"]\n  ),\n  path=\"/video\",\n)\ndef app():\n  with me.box(style=me.Style(margin=me.Margin.all(15))):\n    me.video(\n      src=\"https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm\",\n      style=me.Style(height=300, width=300),\n    )\n
"},{"location":"components/video/#api","title":"API","text":""},{"location":"components/video/#mesop.components.video.video.video","title":"video","text":"

Creates a video.

PARAMETER DESCRIPTION src

URL of the video source

TYPE: str

style

The style to apply to the image, such as width and height.

TYPE: Style | None DEFAULT: None

"},{"location":"getting-started/core-concepts/","title":"Core Concepts","text":"

This doc will explain the core concepts of building a Mesop app.

"},{"location":"getting-started/core-concepts/#hello-world-app","title":"Hello World app","text":"

Let's start by creating a simple Hello World app in Mesop:

import mesop as me\n\n\n@me.page(path=\"/hello_world\")\ndef app():\n  me.text(\"Hello World\")\n

This simple example demonstrates a few things:

  • Every Mesop app starts with import mesop as me. This is the only recommended way to import mesop, otherwise your app may break in the future because you may be relying on internal implementation details.
  • @me.page is a function decorator which makes a function a root component for a particular path. If you omit the path parameter, this is the equivalent of @me.page(path=\"/\").
  • app is a Python function that we will call a component because it's creating Mesop components in the body.
"},{"location":"getting-started/core-concepts/#components","title":"Components","text":"

Components are the building blocks of a Mesop application. A Mesop application is essentially a tree of components.

Let's explain the different kinds of components in Mesop:

  • Mesop comes built-in with native components. These are components implemented using Angular/Javascript. Many of these components wrap Angular Material components.
  • You can also create your own components which are called user-defined components. These are essentially Python functions like app in the previous example.
"},{"location":"getting-started/core-concepts/#counter-app","title":"Counter app","text":"

Let's build a more complex app to demonstrate Mesop's interactivity features.

import mesop as me\n\n\n@me.stateclass\nclass State:\n  clicks: int\n\n\ndef button_click(event: me.ClickEvent):\n  state = me.state(State)\n  state.clicks += 1\n\n\n@me.page(path=\"/counter\")\ndef main():\n  state = me.state(State)\n  me.text(f\"Clicks: {state.clicks}\")\n  me.button(\"Increment\", on_click=button_click)\n

This app allows the user to click on a button and increment a counter, which is shown to the user as \"Clicks: #\".

Let's walk through this step-by-step.

"},{"location":"getting-started/core-concepts/#state","title":"State","text":"

The State class represents the application state for a particular browser session. This means every user session has its own instance of State.

@me.stateclass is a class decorator which is similar to Python's dataclass but also sets default values based on type hints and allows Mesop to inject the class as shown next.

Note: Everything in a state class must be serializable because it's sent between the server and browser.

"},{"location":"getting-started/core-concepts/#event-handler","title":"Event handler","text":"

The button_click function is an event handler. An event handler has a single parameter, event, which can contain a value (this will be shown in the next example). An event handler is responsible for updating state based on the incoming event.

me.state(State) retrieves the instance of the state class for the current session.

"},{"location":"getting-started/core-concepts/#component","title":"Component","text":"

Like the previous example, main is a Mesop component function which is decorated with page to mark it as a root component for a path.

Similar to the event handler, we can retrieve the state in a component function by calling me.state(State).

Note: it's not safe to mutate state inside a component function. All mutations must be done in an event handler.

Rendering dynamic values in Mesop is simple because you can use standard Python string interpolation use f-strings:

me.text(f\"Clicks: {state.clicks}\")\n

The button component demonstrates connecting an event handler to a component. Whenever a click event is triggered by the component, the registered event handler function is called:

me.button(\"Increment\", on_click=button_click)\n

In summary, you've learned how to define a state class, an event handler and wire them together using interactive components.

"},{"location":"getting-started/core-concepts/#whats-next","title":"What's next","text":"

At this point, you've learned all the basics of building a Mesop app. For a step-by-step guide for building a real-world Mesop application, check out the DuoChat Codelab:

DuoChat Codelab

"},{"location":"getting-started/installing/","title":"Installing","text":"

If you are familiar with setting up a Python environment, then run the following command in your terminal:

pip install mesop\n

If you're not familiar with setting up a Python environment, follow one of the options below.

"},{"location":"getting-started/installing/#a-colab-recommended-for-beginners","title":"A. Colab (Recommended for beginners)","text":"

Colab is a free hosted Jupyter notebook product provided by Google.

Try Mesop on Colab:

"},{"location":"getting-started/installing/#b-command-line","title":"B. Command-line","text":"

If you'd like to run Mesop locally on the command-line, follow these steps.

Pre-requisites: Make sure you have Python version 3.10 or later installed by running:

python --version\n

If you don't, please download Python.

"},{"location":"getting-started/installing/#create-a-venv-environment","title":"Create a venv environment","text":"
  1. Open the terminal and navigate to a directory: cd foo

  2. Create a virtual environment by using venv, which will avoid Python environment issues. Run:

python -m venv .venv\n
  1. Activate your virtual environment:

    macOS and LinuxWindows command promptWindows PowerShell
    source .venv/bin/activate\n
    .venv\\Scripts\\activate.bat\n
    .venv\\Scripts\\Activate.ps1\n

Once you've activated the virtual environment, you will see \".venv\" at the start of your terminal prompt.

  1. Install mesop:
pip install mesop\n
"},{"location":"getting-started/installing/#upgrading","title":"Upgrading","text":"

To upgrade Mesop, run the following command:

pip install --upgrade mesop\n

If you are using requirements.txt or pyproject.toml to manage your dependency versions, then you should update those.

"},{"location":"getting-started/installing/#next-steps","title":"Next steps","text":"

Follow the quickstart guide to learn how to create and run a Mesop app:

Quickstart

"},{"location":"getting-started/quickstart/","title":"Quickstart","text":"

Let's build a simple interactive Mesop app.

"},{"location":"getting-started/quickstart/#before-you-start","title":"Before you start","text":"

Make sure you've installed Mesop, otherwise please follow the Installing Guide.

"},{"location":"getting-started/quickstart/#starter-kit","title":"Starter kit","text":"

The simplest way to get started with Mesop is to use the starter kit by running mesop init. You can also copy and paste the code.

import time\n\nimport mesop as me\n\n\n@me.stateclass\nclass State:\n  input: str\n  output: str\n  in_progress: bool\n\n\n@me.page(path=\"/starter_kit\")\ndef page():\n  with me.box(\n    style=me.Style(\n      background=\"#fff\",\n      min_height=\"calc(100% - 48px)\",\n      padding=me.Padding(bottom=16),\n    )\n  ):\n    with me.box(\n      style=me.Style(\n        width=\"min(720px, 100%)\",\n        margin=me.Margin.symmetric(horizontal=\"auto\"),\n        padding=me.Padding.symmetric(\n          horizontal=16,\n        ),\n      )\n    ):\n      header_text()\n      example_row()\n      chat_input()\n      output()\n  footer()\n\n\ndef header_text():\n  with me.box(\n    style=me.Style(\n      padding=me.Padding(\n        top=64,\n        bottom=36,\n      ),\n    )\n  ):\n    me.text(\n      \"Mesop Starter Kit\",\n      style=me.Style(\n        font_size=36,\n        font_weight=700,\n        background=\"linear-gradient(90deg, #4285F4, #AA5CDB, #DB4437) text\",\n        color=\"transparent\",\n      ),\n    )\n\n\nEXAMPLES = [\n  \"How to tie a shoe\",\n  \"Make a brownie recipe\",\n  \"Write an email asking for a sick day off\",\n]\n\n\ndef example_row():\n  is_mobile = me.viewport_size().width < 640\n  with me.box(\n    style=me.Style(\n      display=\"flex\",\n      flex_direction=\"column\" if is_mobile else \"row\",\n      gap=24,\n      margin=me.Margin(bottom=36),\n    )\n  ):\n    for example in EXAMPLES:\n      example_box(example, is_mobile)\n\n\ndef example_box(example: str, is_mobile: bool):\n  with me.box(\n    style=me.Style(\n      width=\"100%\" if is_mobile else 200,\n      height=140,\n      background=\"#F0F4F9\",\n      padding=me.Padding.all(16),\n      font_weight=500,\n      line_height=\"1.5\",\n      border_radius=16,\n      cursor=\"pointer\",\n    ),\n    key=example,\n    on_click=click_example_box,\n  ):\n    me.text(example)\n\n\ndef click_example_box(e: me.ClickEvent):\n  state = me.state(State)\n  state.input = e.key\n\n\ndef chat_input():\n  state = me.state(State)\n  with me.box(\n    style=me.Style(\n      padding=me.Padding.all(8),\n      background=\"white\",\n      display=\"flex\",\n      width=\"100%\",\n      border=me.Border.all(\n        me.BorderSide(width=0, style=\"solid\", color=\"black\")\n      ),\n      border_radius=12,\n      box_shadow=\"0 10px 20px #0000000a, 0 2px 6px #0000000a, 0 0 1px #0000000a\",\n    )\n  ):\n    with me.box(\n      style=me.Style(\n        flex_grow=1,\n      )\n    ):\n      me.native_textarea(\n        value=state.input,\n        autosize=True,\n        min_rows=4,\n        placeholder=\"Enter your prompt\",\n        style=me.Style(\n          padding=me.Padding(top=16, left=16),\n          background=\"white\",\n          outline=\"none\",\n          width=\"100%\",\n          overflow_y=\"auto\",\n          border=me.Border.all(\n            me.BorderSide(style=\"none\"),\n          ),\n        ),\n        on_blur=textarea_on_blur,\n      )\n    with me.content_button(type=\"icon\", on_click=click_send):\n      me.icon(\"send\")\n\n\ndef textarea_on_blur(e: me.InputBlurEvent):\n  state = me.state(State)\n  state.input = e.value\n\n\ndef click_send(e: me.ClickEvent):\n  state = me.state(State)\n  if not state.input:\n    return\n  state.in_progress = True\n  input = state.input\n  state.input = \"\"\n  yield\n\n  for chunk in call_api(input):\n    state.output += chunk\n    yield\n  state.in_progress = False\n  yield\n\n\ndef call_api(input):\n  # Replace this with an actual API call\n  time.sleep(0.5)\n  yield \"Example of streaming an output\"\n  time.sleep(1)\n  yield \"\\n\\nOutput: \" + input\n\n\ndef output():\n  state = me.state(State)\n  if state.output or state.in_progress:\n    with me.box(\n      style=me.Style(\n        background=\"#F0F4F9\",\n        padding=me.Padding.all(16),\n        border_radius=16,\n        margin=me.Margin(top=36),\n      )\n    ):\n      if state.output:\n        me.markdown(state.output)\n      if state.in_progress:\n        with me.box(style=me.Style(margin=me.Margin(top=16))):\n          me.progress_spinner()\n\n\ndef footer():\n  with me.box(\n    style=me.Style(\n      position=\"sticky\",\n      bottom=0,\n      padding=me.Padding.symmetric(vertical=16, horizontal=16),\n      width=\"100%\",\n      background=\"#F0F4F9\",\n      font_size=14,\n    )\n  ):\n    me.html(\n      \"Made with <a href='https://google.github.io/mesop/'>Mesop</a>\",\n    )\n
"},{"location":"getting-started/quickstart/#running-a-mesop-app","title":"Running a Mesop app","text":"

Once you've created your Mesop app using the starter kit, you can run the Mesop app by running the following command in your terminal:

mesop main.py\n

If you've named it something else, replace main.py with the filename of your Python module.

Open the URL printed in the terminal (i.e. http://localhost:32123) in the browser to see your Mesop app loaded.

"},{"location":"getting-started/quickstart/#hot-reload","title":"Hot reload","text":"

If you make changes to the code, the Mesop app should be automatically hot reloaded. This means that you can keep the mesop CLI command running in the background in your terminal and your UI will automatically be updated in the browser.

"},{"location":"getting-started/quickstart/#next-steps","title":"Next steps","text":"

Learn more about the core concepts of Mesop as you learn how to build your own Mesop app:

Core Concepts

"},{"location":"guides/auth/","title":"Auth","text":"

To ensure that the users of your Mesop application are authenticated, this guide provides a detailed, step-by-step process on how to integrate Firebase Authentication with Mesop using a web component.

Mesop is designed to be auth provider agnostic, allowing you to integrate any auth library you prefer, whether it's on the client-side (JavaScript) or server-side (Python). You can support sign-ins, including social sign-ins like Google's or any others that you prefer. The general approach involves signing in on the client-side first, then transmitting an auth token to the server-side.

"},{"location":"guides/auth/#firebase-authentication","title":"Firebase Authentication","text":"

This guide will walk you through the process of integrating Firebase Authentication with Mesop using a custom web component.

Pre-requisites: You will need to create a Firebase account and project. It's free to get started with Firebase and use Firebase auth for small projects, but refer to the pricing page for the most up-to-date information.

We will be using three libraries from Firebase to build an end-to-end auth flow:

  • Firebase Web SDK: Allows you to call Firebase services from your client-side JavaScript code.
  • FirebaseUI Web: Provides a simple, customizable auth UI integrated with the Firebase Web SDK.
  • Firebase Admin SDK (Python): Provides server-side libraries to integrate Firebase services, including Authentication, into your Python applications.

Let's dive into how we will use each one in our Mesop app.

"},{"location":"guides/auth/#web-component","title":"Web component","text":"

The Firebase Authentication web component is a custom component built for handling the user authentication process. It's implemented using Lit, a simple library for building lightweight web components.

"},{"location":"guides/auth/#js-code","title":"JS code","text":"firebase_auth_component.js
import {\n  LitElement,\n  html,\n} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';\n\nimport 'https://www.gstatic.com/firebasejs/10.0.0/firebase-app-compat.js';\nimport 'https://www.gstatic.com/firebasejs/10.0.0/firebase-auth-compat.js';\nimport 'https://www.gstatic.com/firebasejs/ui/6.1.0/firebase-ui-auth.js';\n\n// TODO: replace this with your web app's Firebase configuration\nconst firebaseConfig = {\n  apiKey: 'AIzaSyAQR9T7sk1lElXTEUBYHx7jv7d_Bs2zt-s',\n  authDomain: 'mesop-auth-test.firebaseapp.com',\n  projectId: 'mesop-auth-test',\n  storageBucket: 'mesop-auth-test.appspot.com',\n  messagingSenderId: '565166920272',\n  appId: '1:565166920272:web:4275481621d8e5ba91b755',\n};\n\n// Initialize Firebase\nfirebase.initializeApp(firebaseConfig);\n\nconst uiConfig = {\n  // TODO: change this to your Mesop page path.\n  signInSuccessUrl: '/web_component/firebase_auth/firebase_auth_app',\n  signInFlow: 'popup',\n  signInOptions: [firebase.auth.GoogleAuthProvider.PROVIDER_ID],\n  // tosUrl and privacyPolicyUrl accept either url string or a callback\n  // function.\n  // Terms of service url/callback.\n  tosUrl: '<your-tos-url>',\n  // Privacy policy url/callback.\n  privacyPolicyUrl: () => {\n    window.location.assign('<your-privacy-policy-url>');\n  },\n};\n\n// Initialize the FirebaseUI Widget using Firebase.\nconst ui = new firebaseui.auth.AuthUI(firebase.auth());\n\nclass FirebaseAuthComponent extends LitElement {\n  static properties = {\n    isSignedIn: {type: Boolean},\n    authChanged: {type: String},\n  };\n\n  constructor() {\n    super();\n    this.isSignedIn = false;\n  }\n\n  createRenderRoot() {\n    // Render in light DOM so firebase-ui-auth works.\n    return this;\n  }\n\n  firstUpdated() {\n    firebase.auth().onAuthStateChanged(\n      async (user) => {\n        if (user) {\n          this.isSignedIn = true;\n          const token = await user.getIdToken();\n          this.dispatchEvent(new MesopEvent(this.authChanged, token));\n        } else {\n          this.isSignedIn = false;\n          this.dispatchEvent(new MesopEvent(this.authChanged, ''));\n        }\n      },\n      (error) => {\n        console.log(error);\n      },\n    );\n\n    ui.start('#firebaseui-auth-container', uiConfig);\n  }\n\n  signOut() {\n    firebase.auth().signOut();\n  }\n\n  render() {\n    return html`\n      <div\n        id=\"firebaseui-auth-container\"\n        style=\"${this.isSignedIn ? 'display: none' : ''}\"\n      ></div>\n      <div\n        class=\"firebaseui-container firebaseui-page-provider-sign-in firebaseui-id-page-provider-sign-in firebaseui-use-spinner\"\n        style=\"${this.isSignedIn ? '' : 'display: none'}\"\n      >\n        <button\n          style=\"background-color:#ffffff\"\n          class=\"firebaseui-idp-button mdl-button mdl-js-button mdl-button--raised firebaseui-idp-google firebaseui-id-idp-button\"\n          @click=\"${this.signOut}\"\n        >\n          <span class=\"firebaseui-idp-text firebaseui-idp-text-long\"\n            >Sign out</span\n          >\n        </button>\n      </div>\n    `;\n  }\n}\n\ncustomElements.define('firebase-auth-component', FirebaseAuthComponent);\n

What you need to do:

  • Replace firebaseConfig with your Firebase project's config. Read the Firebase docs to learn how to get yours.
  • Replace the URLs signInSuccessUrl with your Mesop page path and tosUrl and privacyPolicyUrl to your terms and services and privacy policy page respectively.

How it works:

  • This creates a simple and configurable auth UI using FirebaseUI Web.
  • Once the user has signed in, then a sign out button is shown.
  • Whenever the user signs in or out, the web component dispatches an event to the Mesop server with the auth token, or absence of it.
  • See our web component docs for more details.
"},{"location":"guides/auth/#python-code","title":"Python code","text":"firebase_auth_component.py
from typing import Any, Callable\n\nimport mesop.labs as mel\n\n\n@mel.web_component(path=\"./firebase_auth_component.js\")\ndef firebase_auth_component(on_auth_changed: Callable[[mel.WebEvent], Any]):\n  return mel.insert_web_component(\n    name=\"firebase-auth-component\",\n    events={\n      \"authChanged\": on_auth_changed,\n    },\n  )\n

How it works:

  • Implements the Python side of the Mesop web component. See our web component docs for more details.
"},{"location":"guides/auth/#integrating-into-the-app","title":"Integrating into the app","text":"

Let's put it all together:

firebase_auth_app.py
import firebase_admin\nfrom firebase_admin import auth\n\nimport mesop as me\nimport mesop.labs as mel\nfrom mesop.examples.web_component.firebase_auth.firebase_auth_component import (\n  firebase_auth_component,\n)\n\n# Avoid re-initializing firebase app (useful for avoiding warning message because of hot reloads).\nif firebase_admin._DEFAULT_APP_NAME not in firebase_admin._apps:\n  default_app = firebase_admin.initialize_app()\n\n\n@me.page(\n  path=\"/web_component/firebase_auth/firebase_auth_app\",\n  stylesheets=[\n    \"https://www.gstatic.com/firebasejs/ui/6.1.0/firebase-ui-auth.css\"\n  ],\n  # Loosen the security policy so the firebase JS libraries work.\n  security_policy=me.SecurityPolicy(\n    dangerously_disable_trusted_types=True,\n    allowed_connect_srcs=[\"*.googleapis.com\"],\n    allowed_script_srcs=[\n      \"*.google.com\",\n      \"https://www.gstatic.com\",\n      \"https://cdn.jsdelivr.net\",\n    ],\n  ),\n)\ndef page():\n  email = me.state(State).email\n  if email:\n    me.text(\"Signed in email: \" + email)\n  else:\n    me.text(\"Not signed in\")\n  firebase_auth_component(on_auth_changed=on_auth_changed)\n\n\n@me.stateclass\nclass State:\n  email: str\n\n\ndef on_auth_changed(e: mel.WebEvent):\n  firebaseAuthToken = e.value\n  if not firebaseAuthToken:\n    me.state(State).email = \"\"\n    return\n\n  decoded_token = auth.verify_id_token(firebaseAuthToken)\n  # You can do an allowlist if needed.\n  # if decoded_token[\"email\"] != \"allowlisted.user@gmail.com\":\n  #   raise me.MesopUserException(\"Invalid user: \" + decoded_token[\"email\"])\n  me.state(State).email = decoded_token[\"email\"]\n

Note You must add firebase-admin to your Mesop app's requirements.txt file

How it works:

  • The firebase_auth_app.py module integrates the Firebase Auth web component into the Mesop app. It initializes the Firebase app, defines the page where the Firebase Auth web component will be used, and sets up the state to store the user's email.
  • The on_auth_changed function is triggered whenever the user's authentication state changes. If the user is signed in, it verifies the user's ID token and stores the user's email in the state. If the user is not signed in, it clears the email from the state.
"},{"location":"guides/auth/#next-steps","title":"Next steps","text":"

Congrats! You've now built an authenticated app with Mesop from start to finish. Read the Firebase Auth docs to learn how to configure additional sign-in options and much more.

"},{"location":"guides/debugging/","title":"Debugging","text":"

This guide will show you several ways of debugging your Mesop app:

  • Debugging with server logs
  • Debugging with Chrome DevTools
  • Debugging with VS Code

You can use the first two methods to debug your Mesop app both locally and in production, and the last one to debug your Mesop app locally.

"},{"location":"guides/debugging/#debugging-with-server-logs","title":"Debugging with server logs","text":"

If your Mesop app is not working properly, we recommend checking the server logs first.

If you're running Mesop locally, you can check the terminal. If you're running Mesop in production, you will need to use your cloud provider's console to check the logs.

"},{"location":"guides/debugging/#debugging-with-chrome-devtools","title":"Debugging with Chrome DevTools","text":"

Chrome DevTools is a powerful set of web developer tools built directly into the Google Chrome browser. It can be incredibly useful for debugging Mesop applications, especially when it comes to inspecting the client-server interactions.

Here's how you can use Chrome DevTools to debug your Mesop app:

  1. Open your Mesop app in Google Chrome.

  2. Right-click anywhere on the page and select \"Inspect\" or use the keyboard shortcut to open Chrome DevTools:

    • Windows/Linux: Ctrl + Shift + I
    • macOS: Cmd + Option + I
  3. To debug general errors:

    • Go to the Console tab.
    • Look for any console error messages.
    • You can also modify the log levels to display Mesop debug logs by clicking on \"Default levels\" and selecting \"Verbose\".
  4. To debug network issues:

    • Go to the Network tab.
    • Reload your page to see all network requests.
    • Look for any failed requests (they'll be in red).
    • Click on a request to see detailed information about headers, response, etc.
  5. For JavaScript errors: - Check the Console tab for any error messages. - You can set breakpoints in your JavaScript code using the Sources tab.

Remember, while Mesop abstracts away much of the frontend complexity, using these tools can still be valuable for debugging and optimizing your app's performance.

"},{"location":"guides/debugging/#debugging-with-vs-code","title":"Debugging with VS Code","text":"

VS Code is recommended for debugging your Mesop app, but you can also debug Mesop apps in other IDEs.

Pre-requisite: Ensure VS Code is downloaded.

  1. Install the Python Debugger VS Code extension.

  2. Include the following in your .vscode/launch.json:

{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Python: Remote Attach\",\n      \"type\": \"python\",\n      \"request\": \"attach\",\n      \"connect\": { \"host\": \"localhost\", \"port\": 5678 },\n      \"pathMappings\": [\n        { \"localRoot\": \"${workspaceFolder}\", \"remoteRoot\": \".\" }\n      ],\n      \"justMyCode\": true\n    }\n  ]\n}\n
  1. At the top of your Mesop app (e.g. main.py), including the following snippet to start the debug server:
import debugpy\n\ndebugpy.listen(5678)\n
  1. Connect to your debug server by going to the Run & Debug tab in VS Code and selecting \"Python: Remote Attach\".

Congrats you are now debugging your Mesop app!

To learn more about Python debugging in VS code, read VS Code's Python debugging guide.

"},{"location":"guides/deployment/","title":"Deployment","text":"

We recommend Google Cloud Run or Hugging Face Spaces, which both have a free tier.

This section describes how to run your Mesop application using the following platforms:

  • Google Cloud Run
  • Google App Engine
  • Docker
  • Hugging Face Spaces

If you can run your Mesop app on Docker, you should be able to run it on many other cloud platforms, such as Hugging Face Spaces.

"},{"location":"guides/deployment/#example-application","title":"Example application","text":"

Let's start with an example application which will consist of the following files:

  • main.py
  • requirements.txt
"},{"location":"guides/deployment/#mainpy","title":"main.py","text":"

This file contains your Mesop application code:

main.py
import mesop as me\n\n@me.page(title=\"Home\")\ndef home():\n  me.text(\"Hello, world\")\n
"},{"location":"guides/deployment/#requirementstxt","title":"requirements.txt","text":"

This file specifies the Python dependencies needed. You may need to add additional dependencies depending on your use case.

requirements.txt
mesop\ngunicorn\n
"},{"location":"guides/deployment/#cloud-run","title":"Cloud Run","text":"

We recommend using Google Cloud Run because it's easy to get started and there's a free tier.

"},{"location":"guides/deployment/#pre-requisites","title":"Pre-requisites","text":"

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

"},{"location":"guides/deployment/#procfile","title":"Procfile","text":"

Create Procfile to configure gunicorn to run Mesop.

Procfile
web: gunicorn --bind :8080 main:me\n

The --bind: 8080 will run Mesop on port 8080.

The main:me syntax is $(MODULE_NAME):$(VARIABLE_NAME): (see Gunicorn docs for more details):

  • Because the Mesop python file is main.py, the module name is main.
  • By convention, we do import mesop as me so the me refers to the main Mesop library module which is also a callable (e.g. a function) that conforms to WSGI.
"},{"location":"guides/deployment/#deploy-to-google-cloud-run","title":"Deploy to Google Cloud Run","text":"

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

gcloud run deploy\n

Follow the instructions and then you should be able to access your deployed app.

"},{"location":"guides/deployment/#session-affinity","title":"Session Affinity","text":"

If you're running Mesop with MESOP_STATE_SESSION_BACKEND=memory, then you will want to enable session affinity in order to utilize the memory backend efficiently.

The command should be:

gcloud run services update $YOUR_SERVICE --session-affinity\n

By default gunicorn allocates one worker, but you should double check that gunicorn is configured correctly for the memory backend.

"},{"location":"guides/deployment/#app-engine","title":"App Engine","text":"

This section describes deployment to Google App Engine using their flexible environments feature.

"},{"location":"guides/deployment/#pre-requisites_1","title":"Pre-requisites","text":"

You will need to create a Google Cloud account and install the gcloud CLI. See the official documentation for detailed instructions.

You will also need to run:

gcloud app create --project=[YOUR_PROJECT_ID]\ngcloud components install app-engine-python\n
"},{"location":"guides/deployment/#appyaml","title":"app.yaml","text":"

Create app.yaml to configure App Engine to run Mesop.

app.yaml
runtime: python\nenv: flex\nentrypoint: gunicorn -b :$PORT main:me\n\nruntime_config:\n  operating_system: ubuntu22\n  runtime_version: \"3.10\"\n\nmanual_scaling:\n  instances: 1\n\nresources:\n  cpu: 1\n  memory_gb: 0.5\n  disk_size_gb: 10\n
"},{"location":"guides/deployment/#deploy-to-app-engine","title":"Deploy to App Engine","text":"

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

gcloud app deploy\n

Follow the instructions and then you should be able to access your deployed app.

"},{"location":"guides/deployment/#docker","title":"Docker","text":"

If you can run your Mesop app on Docker, you should be able to run it on many other cloud platforms.

"},{"location":"guides/deployment/#pre-requisites_2","title":"Pre-requisites","text":"

Make sure Docker and Docker Compose are installed.

"},{"location":"guides/deployment/#dockerfile","title":"Dockerfile","text":"Dockerfile
FROM python:3.10.15-bullseye\n\nRUN apt-get update && \\\n  apt-get install -y \\\n  # General dependencies\n  locales \\\n  locales-all && \\\n  # Clean local repository of package files since they won't be needed anymore.\n  # Make sure this line is called after all apt-get update/install commands have\n  # run.\n  apt-get clean && \\\n  # Also delete the index files which we also don't need anymore.\n  rm -rf /var/lib/apt/lists/*\n\nENV LC_ALL en_US.UTF-8\nENV LANG en_US.UTF-8\nENV LANGUAGE en_US.UTF-8\n\n# Install dependencies\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\n\n# Create non-root user\nRUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop\nUSER mesop\n\n# Add app code here\nCOPY . /srv/mesop-app\nWORKDIR /srv/mesop-app\n\n# Run Mesop through gunicorn. Should be available at localhost:8080\nCMD [\"gunicorn\", \"--bind\", \"0.0.0.0:8080\", \"main:me\"]\n
"},{"location":"guides/deployment/#docker-composeyaml","title":"docker-compose.yaml","text":"docker-compose.yaml
services:\n  mesop-app:\n    build: .\n    ports:\n      - \"8080:8080\"\n
"},{"location":"guides/deployment/#run-docker-image","title":"Run Docker image","text":"

In your terminal, go to the application directory, which has the files listed above.

Run the following command:

docker-compose up -d\n

Alternatively, if you do not want to use Docker Compose, you can run:

docker build -t mesop-app . && docker run -d -p 8080:8080 mesop-app\n

You should now be able to view your Mesop app at http://localhost:8080.

"},{"location":"guides/deployment/#hugging-face-spaces","title":"Hugging Face Spaces","text":"

Hugging Face Spaces has a free tier that gives you 2 vCPU and 16GB RAM, which is plenty for running Mesop applications that leverage generative AI APIs.

"},{"location":"guides/deployment/#pre-requisites_3","title":"Pre-requisites","text":"

This section assumes you already have a free Hugging Face Space account.

"},{"location":"guides/deployment/#create-new-space","title":"Create new Space","text":"

Go to https://huggingface.co/spaces and click Create new Space.

"},{"location":"guides/deployment/#name-your-app-and-use-docker-sdk","title":"Name your app and use Docker SDK","text":"

Name the Space mesop-hello-world you want and select the apache-2.0 license.

Next select the Docker SDK with a blank template.

"},{"location":"guides/deployment/#cpu-basic-and-create-space","title":"CPU Basic and Create Space","text":"

Next make sure that you are using the free CPU Basic plan. Then click Create Space.

"},{"location":"guides/deployment/#clone-your-hugging-face-space-git-repository","title":"Clone your Hugging Face Space Git Repository","text":"

Example command using Git over SSH:

git clone git@hf.co:spaces/<user-name>/mesop-hello-world\ncd mesop-hello-world\n

Note: You'll need to have an SSH key configured on Hugging Face. See https://huggingface.co/docs/hub/en/security-git-ssh.

"},{"location":"guides/deployment/#create-mainpy","title":"Create main.py","text":"

This is the same main.py file shown earlier, except we need to allow Hugging Face to iframe our Mesop app.

main.py
import mesop as me\n\n@me.page(\n  title=\"Home\",\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://huggingface.co\"]\n  ),\n)\ndef home():\n  me.text(\"Hello, world\")\n
"},{"location":"guides/deployment/#create-requirementstxt","title":"Create requirements.txt","text":"

This file is the same as the generic Docker setup:

requirements.txt
mesop\ngunicorn\n
"},{"location":"guides/deployment/#create-dockerfile","title":"Create Dockerfile","text":"

This file is the same as the generic Docker setup:

Dockerfile
FROM python:3.10.15-bullseye\n\nRUN apt-get update && \\\n  apt-get install -y \\\n  # General dependencies\n  locales \\\n  locales-all && \\\n  # Clean local repository of package files since they won't be needed anymore.\n  # Make sure this line is called after all apt-get update/install commands have\n  # run.\n  apt-get clean && \\\n  # Also delete the index files which we also don't need anymore.\n  rm -rf /var/lib/apt/lists/*\n\nENV LC_ALL en_US.UTF-8\nENV LANG en_US.UTF-8\nENV LANGUAGE en_US.UTF-8\n\n# Install dependencies\nCOPY requirements.txt .\nRUN pip install -r requirements.txt\n\n# Create non-root user\nRUN groupadd -g 900 mesop && useradd -u 900 -s /bin/bash -g mesop mesop\nUSER mesop\n\n# Add app code here\nCOPY . /srv/mesop-app\nWORKDIR /srv/mesop-app\n\n# Run Mesop through gunicorn. Should be available at localhost:8080\nCMD [\"gunicorn\", \"--bind\", \"0.0.0.0:8080\", \"main:me\"]\n
"},{"location":"guides/deployment/#add-app_port-in-readmemd","title":"Add app_port in README.md","text":"

Next we will need to open port 8080 which we specified in the Dockerfile. This is done through a config section in the README.md.

README.md
---\ntitle: Mesop Hello World\nemoji: \ud83d\udc20\ncolorFrom: blue\ncolorTo: purple\nsdk: docker\npinned: false\nlicense: apache-2.0\napp_port: 8080\n---\n\nCheck out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference\n
"},{"location":"guides/deployment/#deploy-to-hugging-face-spaces","title":"Deploy to Hugging Face Spaces","text":"

The commands to commit your changes and push to the Hugging Face Spaces git repository are:

git add -A\ngit commit -m \"Add hello world Mesop app\"\ngit push origin main\n
"},{"location":"guides/deployment/#view-deployed-app","title":"View deployed app","text":"

Congratulations! You should now be able to view your app on Hugging Face Spaces.

The URL should be something like this:

https://huggingface.co/spaces/<user-name>/mesop-hello-world\n

"},{"location":"guides/event-handlers/","title":"Event Handlers","text":"

Event handlers are a core part of Mesop and enables you to handle user interactions by writing Python functions which are called by the Mesop framework when a user event is received.

"},{"location":"guides/event-handlers/#how-it-works","title":"How it works","text":"

Let's take a look at a simple example of an event handler:

Simple event handler
def counter():\n    me.button(\"Increment\", on_click=on_click)\n\ndef on_click(event: me.ClickEvent):\n    state = me.state(State)\n    state.count += 1\n\n@me.stateclass\nclass State:\n    count: int = 0\n

Although this example looks simple, there's a lot going on under the hood.

When the counter function is called, it creates an instance of the button component and binds the on_click event handler to it. Because components (and the entire Mesop UI) is serialized and sent to the client, we need a way of serializing the event handler so that when the button is clicked, the correct event handler is called on the server.

We don't actually need to serialize the entire event handler, rather we just need to compute a unique id for the event handler function.

Because Mesop has a stateless architecture, we need a way of computing an id for the event handler function that's stable across Python runtimes. For example, the initial page may be rendered by one Python server, but another server may be used to respond to the user event. This stateless architecture allows Mesop apps to be fault-tolerant and enables simple scaling.

"},{"location":"guides/event-handlers/#types-of-event-handlers","title":"Types of event handlers","text":""},{"location":"guides/event-handlers/#regular-functions","title":"Regular functions","text":"

These are the simplest and most common type of event handlers used. It's essentially a regular Python function which is called by the Mesop framework when a user event is received.

Regular function
def on_click(event: me.ClickEvent):\n    state = me.state(State)\n    state.count += 1\n
"},{"location":"guides/event-handlers/#generator-functions","title":"Generator functions","text":"

Python Generator functions are a powerful tool, which allow you to yield multiple times in a single event handler. This allows you to render intermediate UI states.

Generator function
def on_click(event: me.ClickEvent):\n    state = me.state(State)\n    state.count += 1\n    yield\n    time.sleep(1)\n    state.count += 1\n    yield\n

You can learn more about real-world use cases of the generator functions in the Interactivity guide.

Always yield at the end of a generator function

If you use a yield statement in your event handler, then the event handler will be a generator function. You must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

"},{"location":"guides/event-handlers/#async-generator-functions","title":"Async generator functions","text":"

Python async generator functions allow you to do concurrent work using Python's async and await language features. If you are using async Python libraries, you can use these types of event handlers.

Async generator function
async def on_click(event: me.ClickEvent):\n    state = me.state(State)\n    state.count += 1\n    yield\n    await asyncio.sleep(1)\n    state.count += 1\n    yield\n

For a more complete example, please refer to the Async section of the Interactivity guide.

Always yield at the end of an async generator function

Similar to a regular generator function, an async generator function must have a yield statement at the end of the event handler (or each return point), otherwise not all of your code will be executed.

"},{"location":"guides/event-handlers/#patterns","title":"Patterns","text":""},{"location":"guides/event-handlers/#reusing-event-handler-logic","title":"Reusing event handler logic","text":"

You can share event handler logic by extracting the common logic into a separate function. For example, you will often want to use the same logic for the on_enter event handler for an input component and the on_click event handler for a \"send\" button component.

Reusing event handler logic
def on_enter(event: me.InputEnterEvent):\n    state = me.state(State)\n    state.value = event.value\n    call_api()\n\ndef on_click(event: me.ClickEvent):\n    # Assumes that state.value has been set by an on_blur event handler\n    call_api()\n\ndef call_api():\n    # Put your common event handler logic here\n    pass\n

If you want to reuse event handler logic between generator functions, you can use the yield from syntax. For example, let's say call_api in the above example is a generator function. You can use yield from to reuse the event handler logic:

Reusing event handler logic for generator functions
def on_enter(event: me.InputEnterEvent):\n    state = me.state(State)\n    state.value = event.value\n    yield from call_api()\n\ndef on_click(event: me.ClickEvent):\n    yield from call_api()\n\ndef call_api():\n    # Do initial work\n    yield\n    # Do more work\n    yield\n
"},{"location":"guides/event-handlers/#boilerplate-free-event-handlers","title":"Boilerplate-free event handlers","text":"

If you're building a form-like UI, it can be tedious to write a separate event handler for each form field. Instead, you can use this pattern which utilizes the key attribute that's available in most events and uses Python's built-in setattr function to dynamically update the state:

Boilerplate-free event handlers
def app():\n  me.input(label=\"Name\", key=\"name\", on_blur=update_state)\n  me.input(label=\"Address\", key=\"address\", on_blur=update_state)\n\n@me.stateclass\nclass State:\n  name: str\n  address: str\n\ndef update_state(event: me.InputBlurEvent):\n  state = me.state(State)\n  setattr(state, event.key, event.value)\n

The downside of this approach is that you lose type safety. Generally, defining a separate event handler, although more verbose, is easier to maintain.

"},{"location":"guides/event-handlers/#troubleshooting","title":"Troubleshooting","text":""},{"location":"guides/event-handlers/#avoid-using-closure-variables-in-event-handler","title":"Avoid using closure variables in event handler","text":"

One subtle mistake when building a reusable component is having the event handler use a closure variable, as shown in the following example:

Bad example of using closure variable
@me.component\ndef link_component(url: str):\n   def on_click(event: me.ClickEvent):\n     me.navigate(url)\n  return me.button(url, on_click=on_click)\n\ndef app():\n    link_component(\"/1\")\n    link_component(\"/2\")\n

The problem with this above example is that Mesop only stores the last event handler. This is because each event handler has the same id which means that Mesop cannot differentiate between the two instances of the same event handler.

This means that both instances of the link_component will refer to the last on_click instance which references the same url closure variable set to \"/2\". This almost always produces the wrong behavior.

Instead, you will want to use the pattern of relying on the key in the event handler as demonstrated in the following example:

Good example of using key
@me.component\ndef link_component(url: str):\n   def on_click(event: me.ClickEvent):\n     me.navigate(event.key)\n  return me.button(url, key=url, on_click=on_click)\n

For more info on using component keys, please refer to the Component Key docs.

"},{"location":"guides/event-handlers/#next-steps","title":"Next steps","text":"

Explore advanced interactivity patterns like streaming and async:

Interactivity

"},{"location":"guides/interactivity/","title":"Interactivity","text":"

This guide continues from the event handlers guide and explains advanced interactivity patterns for dealing with common use cases such as calling a slow blocking API call or a streaming API call.

"},{"location":"guides/interactivity/#intermediate-loading-state","title":"Intermediate loading state","text":"

If you are calling a slow blocking API (e.g. several seconds) to provide a better user experience, you may want to introduce a custom loading indicator for a specific event.

Note: Mesop has a built-in loading indicator at the top of the page for all events.

import time\n\nimport mesop as me\n\n\ndef slow_blocking_api_call():\n  time.sleep(2)\n  return \"foo\"\n\n\n@me.stateclass\nclass State:\n  data: str\n  is_loading: bool\n\n\ndef button_click(event: me.ClickEvent):\n  state = me.state(State)\n  state.is_loading = True\n  yield\n  data = slow_blocking_api_call()\n  state.data = data\n  state.is_loading = False\n  yield\n\n\n@me.page(path=\"/loading\")\ndef main():\n  state = me.state(State)\n  if state.is_loading:\n    me.progress_spinner()\n  me.text(state.data)\n  me.button(\"Call API\", on_click=button_click)\n

In this example, our event handler is a Python generator function. Each yield statement yields control back to the Mesop framework and executes a render loop which results in a UI update.

Before the first yield statement, we set is_loading to True on state so we can show a spinner while the user is waiting for the slow API call to complete.

Before the second (and final) yield statement, we set is_loading to False, so we can hide the spinner and then we add the result of the API call to state so we can display that to the user.

Tip: you must have a yield statement as the last line of a generator event handler function. Otherwise, any code after the final yield will not be executed.

"},{"location":"guides/interactivity/#streaming","title":"Streaming","text":"

This example builds off the previous Loading example and makes our event handler a generator function so we can incrementally update the UI.

from time import sleep\n\nimport mesop as me\n\n\ndef generate_str():\n  yield \"foo\"\n  sleep(1)\n  yield \"bar\"\n\n\n@me.stateclass\nclass State:\n  string: str = \"\"\n\n\ndef button_click(action: me.ClickEvent):\n  state = me.state(State)\n  for val in generate_str():\n    state.string += val\n    yield\n\n\n@me.page(path=\"/streaming\")\ndef main():\n  state = me.state(State)\n  me.button(\"click\", on_click=button_click)\n  me.text(text=f\"{state.string}\")\n
"},{"location":"guides/interactivity/#async","title":"Async","text":"

If you want to do multiple long-running operations concurrently, then we recommend you to use Python's async and await.

import asyncio\n\nimport mesop as me\n\n\n@me.page(path=\"/async_await\")\ndef page():\n  s = me.state(State)\n  me.text(\"val1=\" + s.val1)\n  me.text(\"val2=\" + s.val2)\n  me.button(\"async with yield\", on_click=click_async_with_yield)\n  me.button(\"async without yield\", on_click=click_async_no_yield)\n\n\n@me.stateclass\nclass State:\n  val1: str\n  val2: str\n\n\nasync def fetch_dummy_values():\n  # Simulate an asynchronous operation\n  await asyncio.sleep(2)\n  return \"<async_value>\"\n\n\nasync def click_async_with_yield(e: me.ClickEvent):\n  val1_task = asyncio.create_task(fetch_dummy_values())\n  val2_task = asyncio.create_task(fetch_dummy_values())\n\n  me.state(State).val1, me.state(State).val2 = await asyncio.gather(\n    val1_task, val2_task\n  )\n  yield\n\n\nasync def click_async_no_yield(e: me.ClickEvent):\n  val1_task = asyncio.create_task(fetch_dummy_values())\n  val2_task = asyncio.create_task(fetch_dummy_values())\n\n  me.state(State).val1, me.state(State).val2 = await asyncio.gather(\n    val1_task, val2_task\n  )\n
"},{"location":"guides/interactivity/#troubleshooting","title":"Troubleshooting","text":""},{"location":"guides/interactivity/#user-input-race-condition","title":"User input race condition","text":"

If you notice a race condition with user input (e.g. input or textarea) where sometimes the last few characters typed by the user is lost, you are probably unnecessarily setting the value of the component.

See the following example using this anti-pattern :

Bad example: setting the value and using on_input
@me.stateclass\nclass State:\n  input_value: str\n\ndef app():\n  state = me.state(State)\n  me.input(value=state.input_value, on_input=on_input)\n\ndef on_input(event: me.InputEvent):\n  state = me.state(State)\n  state.input_value = event.value\n

The problem is that the input value now has a race condition because it's being set by two sources:

  1. The server is setting the input value based on state.
  2. The client is setting the input value based on what the user is typing.

There's several ways to fix this which are shown below.

"},{"location":"guides/interactivity/#option-1-use-on_blur-instead-of-on_input","title":"Option 1: Use on_blur instead of on_input","text":"

You can use the on_blur event instead of on_input to only update the input value when the user loses focus on the input field.

This is also more performant because it sends much fewer network requests.

Good example: setting the value and using on_input
@me.stateclass\nclass State:\n  input_value: str\n\ndef app():\n  state = me.state(State)\n  me.input(value=state.input_value, on_input=on_input)\n\ndef on_input(event: me.InputEvent):\n  state = me.state(State)\n  state.input_value = event.value\n
"},{"location":"guides/interactivity/#option-2-do-not-set-the-input-value-from-the-server","title":"Option 2: Do not set the input value from the server","text":"

If you don't need to set the input value from the server, then you can remove the value attribute from the input component.

Good example: not setting the value
@me.stateclass\nclass State:\n  input_value: str\n\ndef app():\n  state = me.state(State)\n  me.input(on_input=on_input)\n\ndef on_input(event: me.InputEvent):\n  state = me.state(State)\n  state.input_value = event.value\n
"},{"location":"guides/interactivity/#option-3-use-two-separate-variables-for-initial-and-current-input-value","title":"Option 3: Use two separate variables for initial and current input value","text":"

If you need set the input value from the server and you need to use on_input, then you can use two separate variables for the initial and current input value.

Good example: using two separate variables for initial and current input value
@me.stateclass\nclass State:\n  initial_input_value: str = \"initial_value\"\n  current_input_value: str\n\n@me.page()\ndef app():\n  state = me.state(State)\n  me.input(value=state.initial_input_value, on_input=on_input)\n\ndef on_input(event: me.InputEvent):\n  state = me.state(State)\n  state.current_input_value = event.value\n
"},{"location":"guides/interactivity/#next-steps","title":"Next steps","text":"

Learn about layouts to build a customized UI.

Layouts

"},{"location":"guides/labs/","title":"Labs","text":"

Mesop Labs is built on top of the core Mesop framework and may evolve in the future.

"},{"location":"guides/labs/#using-labs","title":"Using Labs","text":"

You will need to add an import statement to use labs:

import mesop.labs as mel\n

The code inside Mesop Labs is intended to be simple to understand so you can copy and customize it as needed.

"},{"location":"guides/layouts/","title":"Layouts","text":"

Mesop takes an unopinionated approach to layout. It does not impose a specific layout on your app so you can build custom layouts. The crux of doing layouts in Mesop is the Box component and using the Style API which are Pythonic wrappers around the CSS layout model.

For most Mesop apps, you will use some combination of these types of layouts:

  • Rows and Columns
  • Grids
"},{"location":"guides/layouts/#common-layout-examples","title":"Common layout examples","text":"

To interact with the examples below, open the Mesop Layouts Colab:

"},{"location":"guides/layouts/#rows-and-columns","title":"Rows and Columns","text":""},{"location":"guides/layouts/#basic-row","title":"Basic Row","text":"Basic Row
def row():\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\")):\n        me.text(\"Left\")\n        me.text(\"Right\")\n
"},{"location":"guides/layouts/#row-with-spacing","title":"Row with Spacing","text":"Row with Spacing
def row():\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", justify_content=\"space-around\")):\n        me.text(\"Left\")\n        me.text(\"Right\")\n
"},{"location":"guides/layouts/#row-with-alignment","title":"Row with Alignment","text":"Row with Alignment
def row():\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", align_items=\"center\")):\n        me.box(style=me.Style(background=\"red\", height=50, width=\"50%\"))\n        me.box(style=me.Style(background=\"blue\", height=100, width=\"50%\"))\n
"},{"location":"guides/layouts/#rows-and-columns_1","title":"Rows and Columns","text":"Rows and Columns
def app():\n    with me.box(style=me.Style(display=\"flex\", flex_direction=\"row\", gap=16, height=\"100%\")):\n        column(1)\n        column(2)\n        column(3)\n\ndef column(num: int):\n    with me.box(style=me.Style(\n        flex_grow=1,\n        background=\"#e0e0e0\",\n        padding=me.Padding.all(16),\n        display=\"flex\",\n        flex_direction=\"column\",\n    )):\n        me.box(style=me.Style(background=\"red\", height=100))\n        me.box(style=me.Style(background=\"blue\", flex_grow=1))\n
"},{"location":"guides/layouts/#grids","title":"Grids","text":""},{"location":"guides/layouts/#side-by-side-grid","title":"Side-by-side Grid","text":"Side-by-side Grid
def grid():\n    # 1fr means 1 fraction, so each side is the same size.\n    # Try changing one of the 1fr to 2fr and see what it looks like\n    with me.box(style=me.Style(display=\"grid\", grid_template_columns=\"1fr 1fr\")):\n        me.text(\"A bunch of text\")\n        me.text(\"Some more text\")\n
"},{"location":"guides/layouts/#header-body-footer-grid","title":"Header Body Footer Grid","text":"Header Body Footer Grid
def app():\n    with me.box(style=me.Style(\n        display=\"grid\",\n        grid_template_rows=\"auto 1fr auto\",\n        height=\"100%\"\n    )):\n        # Header\n        with me.box(style=me.Style(\n            background=\"#f0f0f0\",\n            padding=me.Padding.all(24)\n        )):\n            me.text(\"Header\")\n\n        # Body\n        with me.box(style=me.Style(\n            padding=me.Padding.all(24),\n            overflow_y=\"auto\"\n        )):\n            me.text(\"Body Content\")\n            # Add more body content here\n\n        # Footer\n        with me.box(style=me.Style(\n            background=\"#f0f0f0\",\n            padding=me.Padding.all(24)\n        )):\n            me.text(\"Footer\")\n
"},{"location":"guides/layouts/#sidebar-layout","title":"Sidebar Layout","text":"Sidebar Layout
def app():\n    with me.box(style=me.Style(\n        display=\"grid\",\n        grid_template_columns=\"250px 1fr\",\n        height=\"100%\"\n    )):\n        # Sidebar\n        with me.box(style=me.Style(\n            background=\"#f0f0f0\",\n            padding=me.Padding.all(24),\n            overflow_y=\"auto\"\n        )):\n            me.text(\"Sidebar\")\n\n        # Main content\n        with me.box(style=me.Style(\n            padding=me.Padding.all(24),\n            overflow_y=\"auto\"\n        )):\n            me.text(\"Main Content\")\n
"},{"location":"guides/layouts/#responsive-ui","title":"Responsive UI","text":"

This is similar to the Grid Sidebar layout above, except on smaller screens, we will hide the sidebar. Try resizing the browser window and see how the UI changes.

Learn more about responsive UI in the viewport size docs.

def app():\n    is_desktop = me.viewport_size().width > 640\n    with me.box(style=me.Style(\n        display=\"grid\",\n        grid_template_columns=\"250px 1fr\" if is_desktop else \"1fr\",\n        height=\"100%\"\n    )):\n        if is_desktop:\n          # Sidebar\n          with me.box(style=me.Style(\n              background=\"#f0f0f0\",\n              padding=me.Padding.all(24),\n              overflow_y=\"auto\"\n          )):\n              me.text(\"Sidebar\")\n\n        # Main content\n        with me.box(style=me.Style(\n            padding=me.Padding.all(24),\n            overflow_y=\"auto\"\n        )):\n            me.text(\"Main Content\")\n
"},{"location":"guides/layouts/#learn-more","title":"Learn more","text":"

For a real-world example of using these types of layouts, check out the Mesop Showcase app:

  • App
  • Code

To learn more about flexbox layouts (rows and columns), check out:

  • CSS Tricks Guide to Flexbox Layouts
  • MDN Flexbox guide

To learn more about grid layouts, check out:

  • CSS Tricks Guide to Grid Layouts
  • MDN Grid guide
"},{"location":"guides/multi-pages/","title":"Multi-Pages","text":"

You can define multi-page Mesop applications by using the page feature you learned from Core Concepts.

"},{"location":"guides/multi-pages/#multi-page-setup","title":"Multi-page setup","text":"
import mesop as me\n\n@me.page(path=\"/1\")\ndef page1():\n    me.text(\"page 1\")\n\n@me.page(path=\"/2\")\ndef page2():\n    me.text(\"page 2\")\n

Learn more about page configuration in the page API doc.

"},{"location":"guides/multi-pages/#navigation","title":"Navigation","text":"

If you have multiple pages, you will typically want to navigate from one page to another when the user clicks a button. You can use me.navigate(\"/to/path\") to navigate to another page.

Example:

import mesop as me\n\n\ndef on_click(e: me.ClickEvent):\n  state = me.state(State)\n  state.count += 1\n  me.navigate(\"/multi_page_nav/page_2\")\n\n\n@me.page(path=\"/multi_page_nav\")\ndef main_page():\n  me.button(\"Navigate to Page 2\", on_click=on_click)\n\n\n@me.page(path=\"/multi_page_nav/page_2\")\ndef page_2():\n  state = me.state(State)\n  me.text(f\"Page 2 - count: {state.count}\")\n\n\n@me.stateclass\nclass State:\n  count: int\n

Note: you can re-use state across pages. See how the above example uses the State#count value across pages.

"},{"location":"guides/performance/","title":"Performance","text":"

Occasionally, you may run into performance issues with your Mesop app. Here are some tips to help you improve your app's performance.

"},{"location":"guides/performance/#determine-the-root-cause","title":"Determine the root cause","text":"

The first step in debugging performance issues is to identify the cause of the issue. Follow the Debugging with DevTools guide and use the Console and Network tab to identify the issue.

"},{"location":"guides/performance/#common-performance-bottlenecks-and-solutions","title":"Common performance bottlenecks and solutions","text":""},{"location":"guides/performance/#optimizing-state-size","title":"Optimizing state size","text":"

If you notice with Chrome DevTools that you're sending very large network payloads between client and server, it's likely that your state is too large.

Because the state object is serialized and sent back and forth between the client and server, you should try to keep the state object reasonably sized. For example, if you store a very large string (e.g. base64-encoded image) in state, then it will degrade performance of your Mesop app.

The following are recommendations to help you avoid large state payloads:

"},{"location":"guides/performance/#store-state-in-memory","title":"Store state in memory","text":"

Mesop has a feature that allows you to store state in memory rather than passing the full state on every request. This may help improve performance when dealing with large state objects. The caveat is that, storing state in memory contains its own set of problems that you must carefully consider. See the config section for details on how to use this feature.

If you are running Mesop on a single replica or you can enable session affinity, then this is a good option.

"},{"location":"guides/performance/#store-state-externally","title":"Store state externally","text":"

You can also store state outside of Mesop using a database or a storage service. This is a good option if you have a large amount of state data. For example, rather than storing images in the state, you can store them in a bucket service like Google Cloud Storage and send signed URLs to the client so that it can directly fetch the images without going through the Mesop server.

"},{"location":"guides/performance/#handling-high-user-load","title":"Handling high user load","text":"

If you notice that your Mesop app is running slowly when you have many concurrent users, you can try to scale your Mesop app.

"},{"location":"guides/performance/#increase-the-number-of-replicas","title":"Increase the number of replicas","text":"

To handle more concurrent users, you can scale your Mesop app horizontally by increasing the number of replicas (instances) running your application. This can be achieved through various cloud services that offer autoscaling features:

  1. Use a managed service like Google Cloud Run, which automatically scales your app based on traffic. Follow Mesop's Cloud Run deployment guide for details.

  2. Manually adjust the number of replicas to a higher number.

  3. Tune gunicorn settings. If you're using gunicorn to serve your Mesop app, you can adjust gunicorn settings to increase the number of workers. This can help to increase the number of concurrent users your Mesop app can handle.

Whichever platform you choose, make sure to configure the replica settings to match your app's performance requirements and budget constraints.

"},{"location":"guides/server-integration/","title":"Server integration","text":"

Mesop allows you to integrate Mesop with other Python web servers like FastAPI or Flask by mounting the Mesop app which is a WSGI app.

This enables you to do things like:

  • Serve local files (e.g. images)
  • Provide API endpoints (which can be called by the web component, etc.)
"},{"location":"guides/server-integration/#api","title":"API","text":"

The main API for doing this integration is the create_wsgi_app function.

"},{"location":"guides/server-integration/#mesop.server.wsgi_app.create_wsgi_app","title":"create_wsgi_app","text":"

Creates a WSGI app that can be used to run Mesop in a WSGI server like gunicorn.

PARAMETER DESCRIPTION debug_mode

If True, enables debug mode for the Mesop app.

TYPE: bool DEFAULT: False

"},{"location":"guides/server-integration/#fastapi-example","title":"FastAPI example","text":"

For a working example of using Mesop with FastAPI, please take a look at this repo: https://github.com/wwwillchen/mesop-fastapi

Note: you can apply similar steps to use any other web framework that allows you to mount a WSGI app.

"},{"location":"guides/state-management/","title":"State Management","text":"

State management is a critical element of building interactive apps because it allows you store information about what the user did in a structured way.

"},{"location":"guides/state-management/#basic-usage","title":"Basic usage","text":"

You can register a class using the class decorator me.stateclass which is like a dataclass with special powers:

@me.stateclass\nclass State:\n  val: str\n

You can get an instance of the state class inside any of your Mesop component functions by using me.state:

@me.page()\ndef page():\n    state = me.state(State)\n    me.text(state.val)\n
"},{"location":"guides/state-management/#use-immutable-default-values","title":"Use immutable default values","text":"

Similar to regular dataclasses which disallow mutable default values, you need to avoid mutable default values such as list and dict for state classes. Using mutable default values can result in leaking state across sessions which can be a serious privacy issue.

You MUST use immutable default values or use dataclasses field initializer or not set a default value.

Good: immutable default value

Setting a default value to an immutable type like str is OK.

@me.stateclass\nclass State:\n  a: str = \"abc\"\n
Bad: mutable default value

The following will raise an exception because dataclasses prevents you from using mutable collection types like list as the default value because this is a common footgun.

@me.stateclass\nclass State:\n  a: list[str] = [\"abc\"]\n

If you set a default value to an instance of a custom type, an exception will not be raised, but you will be dangerously sharing the same mutable instance across sessions which could cause a serious privacy issue.

@me.stateclass\nclass State:\n  a: MutableClass = MutableClass()\n
Good: default factory

If you want to set a field to a mutable default value, use default_factory in the field function from the dataclasses module to create a new instance of the mutable default value for each instance of the state class.

from dataclasses import field\n\n@me.stateclass\nclass State:\n  a: list[str] = field(default_factory=lambda: [\"abc\"])\n
Good: no default value

If you want a default of an empty list, you can just not define a default value and Mesop will automatically define an empty list default value.

For example, if you write the following:

@me.stateclass\nclass State:\n  a: list[str]\n

It's the equivalent of:

@me.stateclass\nclass State:\n  a: list[str] = field(default_factory=list)\n
"},{"location":"guides/state-management/#how-state-works","title":"How State Works","text":"

me.stateclass is a class decorator which tells Mesop that this class can be retrieved using the me.state method, which will return the state instance for the current user session.

If you are familiar with the dependency injection pattern, Mesop's stateclass and state API is essentially a minimalist dependency injection system which scopes the state object to the lifetime of a user session.

Under the hood, Mesop is sending the state back and forth between the server and browser client so everything in a state class must be serializable.

"},{"location":"guides/state-management/#multiple-state-classes","title":"Multiple state classes","text":"

You can use multiple classes to store state for the current user session.

Using different state classes for different pages or components can help make your app easier to maintain and more modular.

@me.stateclass\nclass PageAState:\n    ...\n\n@me.stateclass\nclass PageBState:\n    ...\n\n@me.page(path=\"/a\")\ndef page_a():\n    state = me.state(PageAState)\n    ...\n\n@me.page(path=\"/b\")\ndef page_b():\n    state = me.state(PageBState)\n    ...\n

Under the hood, Mesop is managing state classes based on the identity (e.g. module name and class name) of the state class, which means that you could have two state classes named \"State\", but if they are in different modules, then they will be treated as separate state, which is what you would expect.

"},{"location":"guides/state-management/#nested-state","title":"Nested State","text":"

You can also have classes inside of a state class as long as everything is serializable:

class NestedState:\n  val: str\n\n@me.stateclass\nclass State:\n  nested: NestedState\n\ndef app():\n  state = me.state(State)\n

Note: you only need to decorate the top-level state class with @me.stateclass. All the nested state classes will automatically be wrapped.

"},{"location":"guides/state-management/#nested-state-and-dataclass","title":"Nested State and dataclass","text":"

Sometimes, you may want to explicitly decorate the nested state class with dataclass because in the previous example, you couldn't directly instantiate NestedState.

If you wanted to use NestedState as a general dataclass, you can do the following:

@dataclass\nclass NestedState:\n  val: str = \"\"\n\n@me.stateclass\nclass State:\n  nested: NestedState\n\ndef app():\n  state = me.state(State)\n

Reminder: because dataclasses do not have default values, you will need to explicitly set default values, otherwise Mesop will not be able to instantiate an empty version of the class.

Now, if you have an event handler function, you can do the following:

def on_click(e):\n    response = call_api()\n    state = me.state(State)\n    state.nested = NestedState(val=response.text)\n

If you didn't explicitly annotate NestedState as a dataclass, then you would get an error instantiating NestedState because there's no initializer defined.

"},{"location":"guides/state-management/#tips","title":"Tips","text":""},{"location":"guides/state-management/#state-performance-issues","title":"State performance issues","text":"

Take a look at the performance guide to learn how to identify and fix State-related performance issues.

"},{"location":"guides/state-management/#next-steps","title":"Next steps","text":"

Event handlers complement state management by providing a way to update your state in response to user interactions.

Event handlers

"},{"location":"guides/static-assets/","title":"Static Assets","text":"

Mesop allows you to specify a folder for storing static assets that will be served by the Mesop server.

This feature provides a simple way to serving images, CSS stylesheets, and other files without having to rely on CDNs, external servers, or mounting Mesop onto FastAPI/Flask.

"},{"location":"guides/static-assets/#enable-a-static-folder","title":"Enable a static folder","text":"

This feature can be enabled using environment variables.

  • MESOP_STATIC_FOLDER
  • MESOP_STATIC_URL_PATH

Full descriptions of these two settings can be found on the config page.

"},{"location":"guides/static-assets/#enabling-a-static-folder-named-assets","title":"Enabling a static folder named \"assets\"","text":"

This will make the files in the assets directory accessible from the Mesop server at /static.

Mesop will look for the assets directory relative to your current working directory. In this case, /some/path/mesop-app/assets.

cd /some/path/mesop-app\nMESOP_STATIC_FOLDER=assets mesop main.py\n

Here is another example:

Mesop will look for the assets directory relative to your current working directory. In this case, /some/path/assets.

cd /some/path\nMESOP_STATIC_FOLDER=assets mesop mesop-app/main.py\n
"},{"location":"guides/static-assets/#enabling-a-static-folder-named-assets-and-url-path-of-assets","title":"Enabling a static folder named \"assets\" and URL path of /assets","text":"

This will make the files in the assets directory accessible from the Mesop server at /assets. For example: https://example.com/assets.

MESOP_STATIC_FOLDER=assets MESOP_STATIC_URL_PATH=/assets mesop main.py\n
"},{"location":"guides/static-assets/#using-a-env-file","title":"Using a .env file","text":"

You can also specify the environment variables in a .env file. This file should be placed in the same directory as the main.py file.

.env
MESOP_STATIC_FOLDER=assets\nMESOP_STATIC_URL_PATH=/assets\n

Then you can run the Mesop command like this:

mesop main.py\n
"},{"location":"guides/static-assets/#example-use-cases","title":"Example use cases","text":"

Here are a couple examples that use the static assets feature.

"},{"location":"guides/static-assets/#add-a-logo","title":"Add a logo","text":"

This example shows you how to load an image to use as a logo for your app.

Let's assume you have a directory like this:

  • static/logo.png
  • main.py
  • requirements.txt

Then you can reference your logo in your Mesop app like this:

main.py
import mesop as me\n\n@me.page()\ndef foo():\n  me.image(src=\"/static/logo.png\")\n
"},{"location":"guides/static-assets/#load-a-tailwind-stylesheet","title":"Load a Tailwind stylesheet","text":"

This example shows you how to use Tailwind CSS with Mesop.

Let's assume you have a directory like this:

  • static/tailwind.css
  • tailwind_input.css
  • tailwind.config.js
  • main.py
  • requirements.txt

You can import the CSS into your page using the stylesheets parameter on @me.page.

main.py
import mesop as me\n\n@me.page(stylesheets=[\"/static/tailwind.css\"])\ndef foo():\n  with me.box(classes=\"bg-gray-800\"):\n    me.text(\"Mesop with Tailwind CSS.\")\n

Tailwind is able to extract the CSS properties from your Mesop main.py file. This does not work for all cases. If you are dynamically generating CSS properties using string concatenation/formatting, then Tailwind may not be able to determine which properties to include. In that case, you may need to manually add these classes to the safelist.

tailwind.config.js
/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: [\"main.py\"],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n  safelist: [],\n};\n

This is just the base Tailwind input file.

tailwind_input.css
@tailwind base;\n@tailwind components;\n@tailwind utilities;\n

The command to generate the output Tailwind CSS is:

# This assumes you have the tailwindcss CLI installed. If not, see\n# https://tailwindcss.com/docs/installation\nnpx tailwindcss -i ./tailwind_input.css -o ./static/tailwind.css\n
"},{"location":"guides/testing/","title":"Testing","text":"

This guide covers the recommended approach for testing Mesop applications using Playwright, a popular browser automation and testing framework.

However, you can use any other testing framework and there's no official testing framework for Mesop.

"},{"location":"guides/testing/#testing-philosophy","title":"Testing Philosophy","text":"

Because Mesop is a full-stack UI framework we recommend doing integration tests to cover critical app functionality. We also recommend separating core business logic into separate Python modules, which do not depend on Mesop. This way you can easily unit test your business logic as well as reuse the business logic as part of scripts or other binaries besides Mesop.

"},{"location":"guides/testing/#playwright-example","title":"Playwright Example","text":"

We will walk through mesop-playwright-testing repo which contains a simple Mesop counter app and a Playwright test written in TypeScript (node.js). Although Playwright has a Python flavor, we recommend using the Playwright node.js flavor because it has better testing support. Even if you're not familiar with JavaScript or TypeScript, it's extremely easy to write Playwright tests because you can generate your tests by clicking through the UI.

The testing example repo's README.md contains instructions for setting up your environment and running the tests.

"},{"location":"guides/testing/#playwright-config","title":"Playwright config","text":"

The playwright configuration used is similar to the default one, however we change a few configurations specific for Mesop.

For example, in playwright.config.ts, we configure Mesop as the local dev server:

  webServer: {\n    command: \"mesop app.py\",\n    url: \"http://127.0.0.1:32123\",\n    reuseExistingServer: !process.env.CI,\n  },\n

This will launch the Mesop app server at the start of the tests.

We also added the following configurations to make writing and debugging tests easier:

  use: {\n    /* Base URL to use in actions like `await page.goto('/')`. */\n    baseURL: \"http://127.0.0.1:32123\",\n\n    /* See https://playwright.dev/docs/trace-viewer */\n    trace: \"retain-on-failure\",\n\n    // Capture screenshot after each test failure.\n    screenshot: \"on\",\n\n    // Retain video on test failure.\n    video: \"retain-on-failure\",\n  },\n
"},{"location":"guides/testing/#running-and-debugging-a-test","title":"Running and debugging a test","text":"

Try changing the test so that it fails. For example, in app.spec.ts change \"Count=1\" to \"Count=2\" and then run the tests: npx playwright test.

The test will fail (as expected) and a new browser page should be opened with the test failure information. You can click on the failing test and view the screenshot, video and trace. This is very helpful in figuring out why a test failed.

"},{"location":"guides/testing/#writing-a-test","title":"Writing a test","text":"

As mentioned above, it's very easy to write Playwright tests because you can generate your tests by clicking through the UI. Even if you're not familiar with JavaScript/TypeScript, you will be able to generate most of the test code by clicking through the UI and copying the generated test code.

Use the Playwright VS Code extension

You can use the Playwright VS Code extension to directly generate test code in your file. You can also run and debug tests from VS Code as well.

"},{"location":"guides/testing/#learn-more","title":"Learn more","text":"

We recommend reading Playwright's docs which are easy to follow and contain much more information on writing browser tests.

You can also look at Mesop's own tests written with Playwright.

"},{"location":"guides/theming/","title":"Theming","text":"

Mesop has early-stage support for theming so you can support light theme and dark theme in a Mesop application.

"},{"location":"guides/theming/#dark-theming","title":"Dark Theming","text":"

For an actual example of using Mesop's theming API to support light theme and dark theme, we will look at the labs chat component which itself is written all in Python built on top of lower-level Mesop components.

"},{"location":"guides/theming/#theme-toggle-button","title":"Theme toggle button","text":"

Inside the chat component, we've defined an icon button to toggle the theme so users can switch between light and dark theme:

def toggle_theme(e: me.ClickEvent):\n    if me.theme_brightness() == \"light\":\n      me.set_theme_mode(\"dark\")\n    else:\n      me.set_theme_mode(\"light\")\n\nwith me.content_button(\n    type=\"icon\",\n    style=me.Style(position=\"absolute\", right=0),\n    on_click=toggle_theme,\n):\n    me.icon(\"light_mode\" if me.theme_brightness() == \"dark\" else \"dark_mode\")\n
"},{"location":"guides/theming/#using-theme-colors","title":"Using theme colors","text":"

You could define custom style logic to explicitly set the color based on the theme:

def container():\n  me.box(\n    style=me.Style(\n      background=\"white\" if me.theme_brightness() == \"light\" else \"black\"\n    )\n  )\n

But this would be pretty tedious, so you can use theme variables like this:

def container():\n  me.box(style=me.Style(background=me.theme_var(\"background\")))\n

This will use the appropriate background color for light theme and dark theme.

"},{"location":"guides/theming/#default-theme-mode","title":"Default theme mode","text":"

Finally, we want to use the default theme mode to \"system\" which means we will use the user's preferences for whether they want dark theme or light theme. For many users, their operating systems will automatically switch to dark theme during night time.

Note: Mesop currently defaults to light theme mode but will eventually default to system theme mode in the future.

On our demo page with the chat component, we have a page on_load event handler defined like this:

def on_load(e: me.LoadEvent):\n  me.set_theme_mode(\"system\")\n
"},{"location":"guides/theming/#theme-density","title":"Theme Density","text":"

You can set the visual density of the Material components. By default, Mesop uses the least visually dense setting, i.e.

me.set_theme_density(0) # 0 is the least dense\n

You can configure the density as an integer from 0 (least dense) to -4 (most dense). For example, if you want a medium-dense UI, you can do the following:

def on_load(e: me.LoadEvent):\n  me.set_theme_density(-2) # -2 is more dense than the default\n\n\n@me.page(on_load=on_load)\ndef page():\n  ...\n
"},{"location":"guides/theming/#api","title":"API","text":""},{"location":"guides/theming/#mesop.features.theme.set_theme_density","title":"set_theme_density","text":"

Sets the theme density for the Material components in the application. A higher density (more negative value) results in a more compact UI layout.

PARAMETER DESCRIPTION density

The desired theme density. It can be 0 (least dense), -1, -2, -3, or -4 (most dense).

TYPE: Literal[0, -1, -2, -3, -4]

"},{"location":"guides/theming/#mesop.features.theme.set_theme_mode","title":"set_theme_mode","text":"

Sets the theme mode for the application.

PARAMETER DESCRIPTION theme_mode

The desired theme mode. It can be \"system\", \"light\", or \"dark\".

TYPE: ThemeMode

"},{"location":"guides/theming/#mesop.features.theme.theme_brightness","title":"theme_brightness","text":"

Returns the current theme brightness.

This function checks the current theme being used by the application and returns whether it is \"light\" or \"dark\".

"},{"location":"guides/theming/#mesop.features.theme.theme_var","title":"theme_var","text":"

Returns the CSS variable for a given theme variable.

PARAMETER DESCRIPTION var

The theme variable name. See the Material Design docs for more information about the colors available.

TYPE: ThemeVar

"},{"location":"guides/theming/#mesop.features.theme.ThemeVar","title":"ThemeVar = Literal['background', 'error', 'error-container', 'inverse-on-surface', 'inverse-primary', 'inverse-surface', 'on-background', 'on-error', 'on-error-container', 'on-primary', 'on-primary-container', 'on-primary-fixed', 'on-primary-fixed-variant', 'on-secondary', 'on-secondary-container', 'on-secondary-fixed', 'on-secondary-fixed-variant', 'on-surface', 'on-surface-variant', 'on-tertiary', 'on-tertiary-container', 'on-tertiary-fixed', 'on-tertiary-fixed-variant', 'outline', 'outline-variant', 'primary', 'primary-container', 'primary-fixed', 'primary-fixed-dim', 'scrim', 'secondary', 'secondary-container', 'secondary-fixed', 'secondary-fixed-dim', 'shadow', 'surface', 'surface-bright', 'surface-container', 'surface-container-high', 'surface-container-highest', 'surface-container-low', 'surface-container-lowest', 'surface-dim', 'surface-tint', 'surface-variant', 'tertiary', 'tertiary-container', 'tertiary-fixed', 'tertiary-fixed-dim'] module-attribute","text":""},{"location":"guides/web-security/","title":"Web Security","text":""},{"location":"guides/web-security/#static-file-serving","title":"Static file serving","text":"

Mesop allows serving JS and CSS files located within the Mesop app's file subtree to support web components.

Security Warning: Do not place any sensitive or confidential JS and CSS files in your Mesop project directory. These files may be inadvertently exposed and served by the Mesop web server, potentially compromising your application's security.

"},{"location":"guides/web-security/#javascript-security","title":"JavaScript Security","text":"

At a high-level, Mesop is built on top of Angular which provides built-in security protections and Mesop configures a strict Content Security Policy.

Specifics:

  • Mesop APIs do not allow arbitrary JavaScript execution in the main execution context. For example, the markdown component sanitizes the markdown content and removes active HTML content like JavaScript.
  • Mesop's default Content Security Policy prevents arbitrary JavaScript code from executing on the page unless it passes Angular's Trusted Types polices.
"},{"location":"guides/web-security/#iframe-security","title":"Iframe Security","text":"

To prevent clickjacking, Mesop apps, when running in prod mode (the default mode used when deployed), do not allow sites from any other origins to iframe the Mesop app.

Note: pages from the same origin as the Mesop app can always iframe the Mesop app.

If you want to allow a trusted site to iframe your Mesop app, you can explicitly allow list the sources which can iframe your app by configuring the security policy for a particular page.

"},{"location":"guides/web-security/#example","title":"Example","text":"
import mesop as me\n\n\n@me.page(\n  path=\"/allows_iframed\",\n  security_policy=me.SecurityPolicy(\n    allowed_iframe_parents=[\"https://google.com\"],\n  ),\n)\ndef app():\n  me.text(\"Test CSP\")\n

You can also use wildcards to allow-list multiple subdomains from the same site, such as: https://*.example.com.

"},{"location":"guides/web-security/#api","title":"API","text":"

You can configure the security policy at the page level. See SecurityPolicy on the Page API docs.

"},{"location":"internal/architecture/","title":"Architecture","text":"

This doc is meant to provide an overview of how Mesop is structured as a framework. It's not necessary to know this information as a developer using Mesop, but if you're developing Mesop's codebase, then this is helpful in laying out the lay of the land.

At the heart of Mesop is two subsystems:

  • A Python server, running on top of Flask.
  • A Web client, built on Angular framework, which wraps various Angular components, particularly Angular Material components.
"},{"location":"internal/architecture/#terminology","title":"Terminology","text":"
  • Downstream - This refers to the synced version of Mesop inside of Google (\"google3 third-party\"). Although almost all the code is shared between the open-source and internal version of Mesop, there's many considerations in maintaining parity between these two versions, particularly with regards to toolchain.
  • Component vs component instance - A component typically refers to the Python factory function that creates a component instance (e.g. me.box()). A component instance refers to a specific component created by a component function and is represented as a Component proto. Other UI frameworks oftentimes give a different name for an instance (i.e. Element) of a component, but for simplicity and explicitness, I will refer to these instances as component instance or component tree (for the entire tree of component instances) in this doc.
"},{"location":"internal/architecture/#life-of-a-mesop-request","title":"Life of a Mesop request","text":""},{"location":"internal/architecture/#initial-page-load","title":"Initial page load","text":"

When a user visits a Mesop application, the following happens:

  1. The user visits a path on the Mesop application, e.g. \"/\" (root path), in their browser.
  2. The Mesop client-side web application (Angular) is bootstrapped and sends an InitRequest to the server.
  3. The Mesop server responds with a RenderEvent which contains a fully instantiated component tree.
  4. The Mesop client renders the component tree. Every Mesop component instance corresponds to 1 or more Angular component instance.
"},{"location":"internal/architecture/#user-interactions","title":"User interactions","text":"

If the user interacts with the Mesop application (e.g. click a button), the following happens:

  1. The user triggers a UserEvent which is sent to the server. The UserEvent includes: the application state (represented by the States proto), the event handler id to trigger, the key of the component interacted with (if any), and the payload value (e.g. for checkbox, it's a bool value which represents the checked state of the checkbox).
  2. The server does the following:
    1. Runs a first render loop in tracing mode (i.e. instantiate the component tree from the root component of the requested path). This discovers any event handler functions. In the future, this trace can also be used to calculate the before component tree so we can calculate the diff of the component tree to minimize the network payload.
    2. Updates the state by feeding the user event to the event handler function discovered in the previous step.

      Note: there's a mapping layer between the UserEvent proto and the granular Python event type. This provides a nicer API for Mesop developers then the internal proto representation.

    3. Runs a second render loop to generate the new component tree given the new state. After the first render loop, each render loop results in a RenderEvent sent to the client.
    4. In the streaming case, we may run the render loop and flush it down via Server-Sent Events many times.
  3. The client re-renders the Angular application after receiving each RenderEvent.
"},{"location":"internal/architecture/#python-server","title":"Python Server","text":"

Flask is a minimalist Python server framework that conforms to WSGI (Web Server Gateway Interface), which is a Python standard that makes it easy for web servers (oftentimes written in other languages like C++) to delegate requests to a Python web framework. This is particularly important in the downstream case because we rely on an internal HTTP server to serve Mesop applications.

For development purposes (i.e. using the CLI), we use Werkzeug, which is a WSGI library included with Flask.

"},{"location":"internal/architecture/#web-client","title":"Web Client","text":"

Mesop's Web client consists of three main parts:

  • Core: Includes the root Angular component and singleton services like Channel. This part is fairly small and is the critical glue between the rest of the client layer and the server.
  • Mesop Components: Every Mesop component has its own directory under /components

    Note: this includes both the Python API and the Angular implementation for developer convenience.

  • Dev Tools: Mesop also comes with a basic set of developer tools, namely the components and log panels. The components panel allows Mesop developers to visualize the component tree. The log panel allows Mesop developers to inspect the application state and component tree values.
"},{"location":"internal/architecture/#static-assets","title":"Static assets","text":"
  • Using the regular CLI, the web client static assets (i.e. JS binary, CSS, images) are served from the Python server. This simplifies deployment of Mesop applications by reducing version skew issues between the client and server.
  • In uncompiled mode (using the dev CLI), the web client is served from the web devserver. This is convenient because it builds faster than the regular/compiled mode and it allows live-reloading when developing the client codebase.
"},{"location":"internal/architecture/#tooling","title":"Tooling","text":"

Outside of the mesop/ directory are various tools used to build, test and document the Mesop framework. However, anything needed to actually run a Mesop application should be located within mesop/. The three main tools inside the codebase are:

  • Build tooling - these are in build_defs/ which contains various Bazel bzl files and tools which is forked from the Angular codebase. The build toolchain is described in more detail on the toolchain doc.
  • Component generator - inside generator/ is a mini-library and CLI tool to generate Mesop components from existing Angular components, specifically Angular Material, although with some modifications it could support more generic Angular components. The generator modifies the codebase so that nothing in generator/ is actually needed when running a Mesop applications.
  • Docs - Mesop's doc site is built using Material for Mkdocs and is what you are looking at right now.
"},{"location":"internal/ci/","title":"CI","text":"

We use GitHub actions. For all third-party GitHub actions, we must pin it to a specific hash to comply with internal policies.

"},{"location":"internal/codespaces/","title":"Development on Github Codespaces","text":"

Github Codespaces is a quick way to get started with internal Mesop development. All you need to do is a click a button and a fully configured workspace will be created for you. No need to spend time debugging installation issues.

Github Free and Pro plans also provide a free tier, so Codespaces is useful for writing and testing quick patches.

If using the free tier, the Codespace setup takes 20-30 minutes due to the limited CPU available.

"},{"location":"internal/codespaces/#create-github-codespace","title":"Create Github Codespace","text":"

You can create a Github Codespace from the Mesop Github repository page.

"},{"location":"internal/codespaces/#wait-for-postcreatecommand-to-run","title":"Wait for postCreateCommand to run","text":"

The Codespace will not be usable until the postCreateCommand has completed. You can view the CLI output by pressing Cmd/Ctrl + Shift + P and then finding the View Creation Log option.

"},{"location":"internal/codespaces/#set-the-python-environment-for-the-codespace","title":"Set the Python environment for the Codespace","text":"

During the postCreateCommand step, you'll see a pop up asking if you want to set a new environment for the Codespace. Select Yes here to use the virtual env that is created as part of the postCreateCommand set up.

"},{"location":"internal/codespaces/#run-mesop-for-development","title":"Run Mesop for development","text":"

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

./scripts/cli.sh\n

This step takes some time for the first run.

You will see some warning messages, but it is OK to ignore them. You can also ignore the message shown in the screenshot.

"},{"location":"internal/codespaces/#view-mesop-demos","title":"View Mesop demos","text":"

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos from the PORTS tab.

"},{"location":"internal/contributing/","title":"How-to Contribute","text":"

Thanks for looking into contributing to Mesop. There's many ways to contribute to Mesop:

  • Filing new issues and providing feedback on existing issues
  • Improving our docs
  • Contributing examples
  • Contributing code

All types of contributions are welcome and are a key piece to making Mesop work well as a project.

"},{"location":"internal/contributing/#before-you-begin","title":"Before you begin","text":""},{"location":"internal/contributing/#sign-our-contributor-license-agreement","title":"Sign our Contributor License Agreement","text":"

Contributions to this project must be accompanied by a Contributor License Agreement (CLA). You (or your employer) retain the copyright to your contribution; this simply gives us permission to use and redistribute your contributions as part of the project.

If you or your current employer have already signed the Google CLA (even if it was for a different project), you probably don't need to do it again.

Visit https://cla.developers.google.com/ to see your current agreements or to sign a new one.

"},{"location":"internal/contributing/#review-our-community-guidelines","title":"Review our community guidelines","text":"

This project follows Google's Open Source Community Guidelines.

"},{"location":"internal/contributing/#contributing-to-docs","title":"Contributing to docs","text":"

If you want to contribute to our docs, please take a look at our docs issues. If you find any of our existing docs confusing or missing key information, please file an issue and we will see how we can improve things. We regularly spend time improving our docs because we know it's a key part of the developer experience.

"},{"location":"internal/contributing/#contributing-examples","title":"Contributing examples","text":"

One of the best way of helping the Mesop project is to share what you've built! You can either add an example to our demo gallery by adding it to the demo/ directory or you can send us a link to your app running and we will include it in our docs.

"},{"location":"internal/contributing/#contributing-code","title":"Contributing code","text":"

If you'd like to contribute code, I recommend taking a look at one of our existing \"starter\" issues. These are issues that are good for first-time contributors as they are well-specified.

  • Setup your dev environment
  • If you're creating a new component, you can follow the new component guide

I recommend reading through the various pages in the contributing section as it will give you a sense of our project's goals.

One thing that we focus on is providing an easy-to-understand API with minimal breaking changes so we ask that any API changes are first discussed in an issue. This will help prevent wasted work because we are conservative with changing our APIs.

"},{"location":"internal/development/","title":"Development","text":"

I recommend following (or at least reading) all the steps in this doc if you plan on actively developing Mesop.

"},{"location":"internal/development/#setup","title":"Setup","text":""},{"location":"internal/development/#bazelibazel","title":"Bazel/ibazel","text":"

We use Bazel as our build system. Use bazelisk which ensures the right version of Bazel is used for this project.

If ibazel breaks, but bazel works, try following these steps

TIP: If your build mysteriously fails due to an npm-related error, try running bazel clean --expunge && rm -rf node_modules. Bazel and Yarn have a cache bug when upgrading npm modules.

"},{"location":"internal/development/#uv","title":"uv","text":"

We use uv. Follow the instructions here to install uv.

"},{"location":"internal/development/#commit-hooks","title":"Commit hooks","text":"
  1. Install pre-commit
  2. Install pre-commit hooks for this repo: pre-commit install
"},{"location":"internal/development/#run-local-development","title":"Run local development","text":"

We recommend using this for most Mesop framework development.

./scripts/cli.sh\n

NOTE: this automatically serves the angular app.

"},{"location":"internal/development/#python","title":"Python","text":""},{"location":"internal/development/#third-party-packages-pip","title":"Third-party packages (PIP)","text":"

If you update //build_defs/requirements.txt, run:

bazel run //build_defs:pip_requirements.update\n
"},{"location":"internal/development/#venv","title":"venv","text":"

To support IDE type-checking (Pylance) in VS Code, we use Aspect's rules_py which generates a venv target.

bazel run //mesop/cli:cli.venv\n

Then, you can activate the venv:

source .cli.venv/bin/activate\n

You will need to setup a symlink to have Python IDE support for protos:

./scripts/setup_proto_py_modules.sh\n

Check that you're using venv's python:

which python\n

Copy the python interpreter path and paste it into VS Code.

Finally, install third-party dependencies.

pip install -r build_defs/requirements_lock.txt\n

NOTE: You may need to run the command with sudo if you get a permission denied error, particularly with \"_distutils_hack\".

"},{"location":"internal/development/#commit-hooks_1","title":"Commit hooks","text":"

We use pre-commit to automatically format, lint code before committing.

Setup:

  1. Install pre-commit.
  2. Setup git hook: pre-commit install
"},{"location":"internal/development/#docs","title":"Docs","text":"

We use Mkdocs Material to generate our docs site.

  1. Activate venv
  2. mkdocs serve
"},{"location":"internal/hot-reload/","title":"Hot Reload","text":"

One of Mesop's key benefits is that it provides a fast iteration cycle through hot reload. This means whenever a Mesop developer changes their Mesop app code, their browser window will automaticall reload and execute the new app code while preserving the existing state. This isn't guaranteed to work, for example, if the State class is modified in an incompatible way, but it should work for >90% of the build-edit loops (e.g. tweaking the UI, calling new components).

"},{"location":"internal/hot-reload/#how-it-works","title":"How it works","text":"

See: https://github.com/google/mesop/pull/211

"},{"location":"internal/hot-reload/#design-decisions","title":"Design decisions","text":""},{"location":"internal/hot-reload/#what-to-reload","title":"What to reload","text":"

Right now we reload all the modules loaded by the Mesop application. However, this results in a lot of unnecessary modules being reloaded and can be quite slow if there's a heavy set of transitive dependencies.

Instead, I'm thinking we can use a heuristic where we calculate the existing package based on the file path passed in and only reload modules which are in the current package or a sub-package. Effectively this is only reloading modules within the target file's subtree.

This seems like a pretty reasonable heuristic where it reloads all the application modules without reloading the entire dependency graph. Previously I tried reloading only the module passed in via --path, however this was too limiting as it meant shared code (e.g. a navmenu) would not get hot-reloaded.

"},{"location":"internal/hot-reload/#when-to-reload","title":"When to reload","text":"

With the previous design decision, re-executing a module should be much faster, but we still need to guard against the case where the live reload occurs too quickly in the client side. Options:

  • Wait a fixed timeout - A simple heuristic could just be to wait 500ms since in theory, all the application code (with the non-application dependnecies cached) should re-execute fairly quickly.
  • Client retry/reload - Another approach could be to retry a client-side reload N times (e.g. 3) if we get an error. The pattern could be: 1. save state to local storage, 2. trigger reload, 3. if reload results in a successful render, we clear the state OR if reload results in an error, we trigger a reload (and persist in local storage which retry attempt this is).
  • Server loop - In the common error case where the server is still re-executing the module and the client reloads, it will hit path not found because the path hasn't been registered yet. One way of mitigating this is to simply do a sleep in debug mode. We can even do an exponential backoff for the sleep (e.g. wait 300ms, 900ms, 2700ms).
  • Preferred appproach - given the trade-offs, I think Server loop is the best option as it's relatively simple to implement, robust and doesn't incur a significant delay in the happy case.
"},{"location":"internal/hot-reload/#abstracting-ibazel-specific-details","title":"Abstracting ibazel-specific details","text":"

Since Google's internal equivalent of ibazel doesn't work exactly the same, we should treat HotReloadService as an abstract base class and then extend it for Ibazel (and the internal variant).

"},{"location":"internal/modes/","title":"Modes","text":"

There are two modes that you can run Mesop in.

"},{"location":"internal/modes/#development-mode-aka-debug-mode-or-editor-mode","title":"Development mode (aka debug mode or editor mode)","text":"

Recommended for developers using Mesop when they are developing the apps locally. This provides good error messages and hot reloading.

  • How to run: ibazel run //mesop/cli -- --path=mesop/mesop/example_index.py
  • Angular should run in dev mode.
  • Developer Tools and Visual Editor are available.
"},{"location":"internal/modes/#prod-mode","title":"Prod mode","text":"

Recommended when developers deploy applications built with Mesop for public serving. This is optimized for performance and provides less-detailed error messages.

  • Developer tools aren't available.
  • Angular doesn't run in dev mode.
  • How to run: bazel run //mesop/cli -- --path=mesop/mesop/example_index.py --prod
"},{"location":"internal/new-component/","title":"New Component","text":""},{"location":"internal/new-component/#how-to","title":"How-to","text":"
python scripts/scaffold_component.py $component_name\n
"},{"location":"internal/new-component/#api-guidelines","title":"API Guidelines","text":"
  • Make all arguments keyword only by putting * as the initial argument. Keyword argument is more readable, particularly for UI components which will have increasingly more optional arguments over time.
  • Model after existing APIs. For example, if we are wrapping an existing @angular/material component, we will try to mirror their API (within reason). If we are wrapping a native HTML element, we should try to expose a similar API. In some cases, we will look at other UI frameworks like Flutter for inspiration, even though we are not directly wrapping them.
  • Prefer small components. We should try to provide small native components that can be composed by content components in Python. This enables a wider range of use cases.
"},{"location":"internal/new-component/#new-events","title":"New events","text":"

Try to reuse the existing events when possible, but you may need to sometimes create a new event.

  1. Define the event class in //mesop/events/{event_name}.py
  2. In the same file, define an event mapper and register it: runtime().register_event(EventClass, event_mapper)
"},{"location":"internal/new-component/#potential-exploration-areas","title":"Potential exploration areas","text":"
  • Code-gen component_renderer using a shell/Python script. Initially, just run the script as-needed, but eventually can run it as part of a BUILD rule (a la @angular/components examples)
"},{"location":"internal/publishing/","title":"Publishing","text":"

Follow these instructions for releasing a new version of Mesop publicly via PyPI (e.g. pip install mesop).

If you haven't done this before, follow the first-time setup.

"},{"location":"internal/publishing/#check-main-branch","title":"Check main branch","text":"

Before, cutting a release, you'll want to check two things:

  1. The main branch should be healthy (e.g. latest commit is green).
  2. Check the snyk dashboard to review security issues: - It only runs weekly so you need to click \"Retest now\". If there's any High security issues for a core Mesop file (e.g. anything in mesop/*), then you should address it before publishing a release.
"},{"location":"internal/publishing/#update-version-to-rc","title":"Update version to RC","text":"

Update mesop/version.py by incrementing the version number. We follow semver.

You want to first create an RC (release candidate) to ensure that it works.

For example, if the current version is: 0.7.0, then you should increment the version to 0.8.0rc1 which will create an RC, which is treated as a pre-release by PyPI.

"},{"location":"internal/publishing/#install-locally","title":"Install locally","text":"

From the workspace root, run the following command:

source ./scripts/pip.sh\n

This will build the Mesop pip package and install it locally so you can test it.

"},{"location":"internal/publishing/#testing-locally","title":"Testing locally","text":"

TIP: Double check the Mesop version is expected. It's easy to use the wrong version of Mesop by loading mesop or gunicorn from a different Python path (i.e. not the venv you just created).

"},{"location":"internal/publishing/#dev-cli","title":"Dev CLI","text":"

The above shell script will tell you to run the following command:

mesop main.py\n

This will start the Mesop dev server and you can test that hot reload works.

"},{"location":"internal/publishing/#gunicorn-integration","title":"Gunicorn integration","text":"
gunicorn main:me\n

Note: gunicorn should already be installed by the shell script above.

"},{"location":"internal/publishing/#upload-to-pypi","title":"Upload to PyPI","text":"

If the testing above looks good, then continue with uploading to PyPI.

rm -rf /tmp/mesoprelease-test/venv-twine \\\n&& virtualenv --python python3 /tmp/mesoprelease-test/venv-twine \\\n&& source /tmp/mesoprelease-test/venv-twine/bin/activate \\\n&& pip install --upgrade pip \\\n&& pip install twine \\\n&& cd /tmp/mesoprelease-test \\\n&& twine upload mesop*.whl\n

Visit https://pypi.org/project/mesop/ to see that the new version has been published.

"},{"location":"internal/publishing/#test-on-colab","title":"Test on Colab","text":"

Because Colab installs from PyPI, you will need to test the RC on Colab after uploading to PyPI.

Open our Mesop Colab notebook. You will need to explicitly pip install the RC version as pip will not automatically install a pre-release version, even if it's the newest version. So change the first cell to something like:

 !pip install mesop==0.X.Yrc1\n

Tip: sometimes it takes a minute for the PyPI registry to be updated after upload, so just try again.

Then, run all the cells and make sure it works. Usually if something breaks in Colab, it's pretty obvious because the output isn't displayed, etc.

"},{"location":"internal/publishing/#change-the-version-from-rc-to-regular-release","title":"Change the version from RC to regular release","text":"

If you find an issue, then redo the above steps and create another RC candidate: 0.8.0rc1 -> 0.8.0rc2.

If all the testing looks good, then you can update mesop/version.py and change the version from RC to a regular release, for example:

0.8.0rc1 -> 0.8.0

Re-do the steps above to build, test and upload it to PyPI.

"},{"location":"internal/publishing/#publish-github-release","title":"Publish GitHub release","text":"

After you've uploaded a new regular release to PyPI, submit the PR which bumps the version and then publish a GitHub release.

  1. Click \"Choose a tag\" and type in the version you just released. This will create a new Git tag.
  2. Click \"Genereate release notes\".
  3. Click \"Create a discussion for this release\".
  4. Click \"Publish release\".
"},{"location":"internal/publishing/#first-time-upload-setup","title":"First-time upload setup","text":"

Create a file ~/.pypirc:

[pypi]\n  username = __token__\n  password = {{password}}\n

You will need to get a PyPI token generated by one of the project maintainers.

"},{"location":"internal/testing/","title":"Testing","text":""},{"location":"internal/testing/#unit-tests","title":"Unit tests","text":"

You can run our unit tests using Bazel.

"},{"location":"internal/testing/#run-tests","title":"Run tests","text":"
bazel test //mesop/...\n
"},{"location":"internal/testing/#e2e-tests","title":"E2E tests","text":"

We use Playwright as our e2e test framework. Unlike most of the stack, this isn't Bazel-ified although we'd like to eventually do this.

"},{"location":"internal/testing/#run-tests_1","title":"Run tests","text":"
yarn playwright test\n
"},{"location":"internal/testing/#debug-tests","title":"Debug tests","text":"
yarn playwright test --debug\n
"},{"location":"internal/toolchain/","title":"Build / Toolchain","text":""},{"location":"internal/toolchain/#context","title":"Context","text":"

Because Mesop is a Google open-source project and we want to provide a good integration with Google's internal codebase, Mesop uses Google's build system Bazel.

Although Bazel is similar to the internal tool, there's numerous differences, particularly around the ecosystems, which makes it quite a challenge to maintain Mesop for both open-source and internal builds. Nevertheless, it's important that we do this to serve both communities well.

"},{"location":"internal/toolchain/#differences","title":"Differences","text":"

We try to isolate as much of the differences between these two environments into the build_defs/ directory. Different versions of the same files inside build_defs/ are maintained for each environment. In particular, build_defs/defaults.bzl is meant to wrap all external rules/macros used by Mesop so we can swap it between the internal and external variants as needed.

Finally, all external dependencies, e.g. Python's requirement('$package') or NPM's @npm//$package, are referenced via an indirection to build_defs/defaults.bzl. This is because Google has a special approach to handling third-party dependencies.

"},{"location":"internal/toolchain/#gotchas","title":"Gotchas","text":"

Here's a quick list of gotchas to watch out for:

  • Do not use import * as when importing protos from TS. This prevents tree-shaking downstream.
  • Do not use any external Bazel references (e.g. @) within mesop/. Instead, reference them indirectly using a wrapper in build_defs/.
  • Avoid relying on implicit transitive dependencies, particularly for TS/NG modules.
  • Do not use raw JSON.parse, instead use jsonParse in strict_types.ts.
"},{"location":"internal/toolchain/#angular","title":"Angular","text":"

We rely heavily on Angular's toolchain, particularly around Bazel integration. Many of the Web-related Bazel rules, particularly for Angular/TS code was forked from github.com/angular/components.

"},{"location":"internal/type-checking/","title":"Type Checking","text":""},{"location":"internal/type-checking/#python-type-checking","title":"Python Type Checking","text":"

For our Python code, we use pyright as our static type checker because it has excellent IDE support in VS Code via pylance.

To run Python type-checking, run:

./scripts/run_py_typecheck.sh\n

This will setup the pre-requisites needed for type-checking.

"},{"location":"internal/vs-code-remote-container/","title":"VS Code Remote Container","text":"

VS Code Remote Containers is a quick way to get started with internal Mesop development if you have VS Code and Docker Desktop installed.

This approach will create a fully configured workspace, saving you time from debugging installation issues and allowing you to start development right away.

"},{"location":"internal/vs-code-remote-container/#pre-requistes-install-vs-code-and-docker","title":"Pre-requistes: Install VS Code and Docker","text":"

In order to use VS Code remote containers, you will need VS Code installed. You will also need Docker Desktop (which will install Docker Engine and Docker Compose) to run the remote containers.

  • VS Code
  • Docker Desktop
"},{"location":"internal/vs-code-remote-container/#fork-and-clone-the-mesop-repository","title":"Fork and Clone the Mesop repository","text":"

It is not recommended to develop locally and on remote containers using the same folder since this may cause unexpected conflicts. Instead you should clone the repository in a separate directory.

You can follow the instructions here on how to fork and clone a Github repository.

"},{"location":"internal/vs-code-remote-container/#share-git-credentials-with-your-container","title":"Share Git credentials with your container","text":"

The VS Code Dev Containers extension provides a few ways to share your local Git credentials with your remote container.

If you cloned the Mesop repo using HTTPS, you can use a Github CLI or Git Credential Manager.

If you used SSH, then your local ssh agent will automatically be forwarded into your remote container. All you need do is run the ssh-add command to add the ssh key you've configured for GitHub access.

See the Sharing Git credentials with your container page for full details.

"},{"location":"internal/vs-code-remote-container/#open-folder-in-container","title":"Open folder in container","text":"

Open VS Code, press Cmd/Ctrl + Shift + P, and select the Dev Containers: Open Folder in Container... option. This will create a new workspace inside a remote container.

"},{"location":"internal/vs-code-remote-container/#wait-for-postcreatecommand-to-run","title":"Wait for postCreateCommand to run","text":"

The workspace will not be usable until the postCreateCommand has completed.

"},{"location":"internal/vs-code-remote-container/#run-mesop-for-development","title":"Run Mesop for development","text":"

Once the postCreateCommand has finished, you can now start Mesop in the terminal.

./scripts/cli.sh\n

You will see some warning messages, but it is OK to ignore them.

You should see this message once the Mesop server is ready.

"},{"location":"internal/vs-code-remote-container/#view-mesop-demos","title":"View Mesop demos","text":"

Once ./scripts/cli.sh has started the Mesop dev server, you can view the demos at http://localhost:32123.

"},{"location":"web-components/","title":"Web Components","text":"

Note: Web components are a new experimental feature released under labs and may have breaking changes.

Mesop allows you to define custom components with web components which is a set of web standards that allows you to use JavaScript and CSS to define custom HTML elements.

"},{"location":"web-components/#use-cases","title":"Use cases","text":"
  • Custom JavaScript - You can execute custom JavaScript and have simple bi-directional communication between the JavaScript code running in the browser and the Python code running the server.

  • JavaScript libraries - If you want to use a JavaScript library, you can wrap them with a web component.

  • Rich-client side interactivity - You can use web components to deliver stateful client-side interactions without a network roundtrip.

"},{"location":"web-components/#anatomy-of-a-web-component","title":"Anatomy of a web component","text":"

Mesop web component consists of two parts:

  • Python module - defines a Python API so that your Mesop app can use the web component seamlessly.
  • JavaScript module - implements the web component.
"},{"location":"web-components/#next-steps","title":"Next steps","text":"

Learn how to build your first web component in the quickstart page.

"},{"location":"web-components/api/","title":"Web Components API","text":"

Note: Web components are a new experimental feature released under labs and may have breaking changes.

Example usage:

import mesop.labs as mel\n\n\n@mel.web_component(...)\ndef a_web_component():\n    mel.insert_web_component(...)\n
"},{"location":"web-components/api/#api","title":"API","text":""},{"location":"web-components/api/#mesop.labs.web_component.web_component","title":"web_component","text":"

A decorator for defining a web component.

This decorator is used to define a web component. It takes a path to the JavaScript file of the web component and an optional parameter to skip validation. It then registers the JavaScript file in the runtime.

PARAMETER DESCRIPTION path

The path to the JavaScript file of the web component.

TYPE: str

skip_validation

If set to True, skips validation. Defaults to False.

TYPE: bool DEFAULT: False

"},{"location":"web-components/api/#mesop.labs.insert_web_component","title":"insert_web_component","text":"

Inserts a web component into the current component tree.

PARAMETER DESCRIPTION name

The name of the web component. This should match the custom element name defined in JavaScript.

TYPE: str

events

A dictionary where the key is the event name, which must match a web component property name defined in JavaScript. The value is the event handler (callback) function. Keys must not be \"src\", \"srcdoc\", or start with \"on\" to avoid web security risks.

TYPE: dict[str, Callable[[WebEvent], Any]] | None DEFAULT: None

properties

A dictionary where the key is the web component property name that's defined in JavaScript and the value is the property value which is plumbed to the JavaScript component. Keys must not be \"src\", \"srcdoc\", or start with \"on\" to avoid web security risks.

TYPE: dict[str, Any] | None DEFAULT: None

key

A unique identifier for the web component. Defaults to None.

TYPE: str | None DEFAULT: None

"},{"location":"web-components/api/#mesop.labs.WebEvent","title":"WebEvent dataclass","text":"

Bases: MesopEvent

An event emitted by a web component.

ATTRIBUTE DESCRIPTION value

The value associated with the web event.

TYPE: Any

key

key of the component that emitted this event.

TYPE: str

"},{"location":"web-components/api/#mesop.slot","title":"slot","text":"

This function is used when defining a content component to mark a place in the component tree where content can be provided by a child component.

"},{"location":"web-components/quickstart/","title":"Quickstart","text":"

Note: Web components are a new experimental feature released under labs and may have breaking changes.

You will learn how to build your first web component step-by-step, a counter component.

Although it's a simple example, it will show you the core APIs of defining your own web component and how to support bi-directional communication between the Python code running on the server and JavaScript code running on the browser.

"},{"location":"web-components/quickstart/#python-module","title":"Python module","text":"

Let's first take a look at the Python module which defines the interface so that the rest of your Mesop app can call the web component in a Pythonic way.

counter_component.py
from typing import Any, Callable\n\nimport mesop.labs as mel\n\n\n@mel.web_component(path=\"./counter_component.js\")\ndef counter_component(\n  *,\n  value: int,\n  on_decrement: Callable[[mel.WebEvent], Any],\n  key: str | None = None,\n):\n  return mel.insert_web_component(\n    name=\"quickstart-counter-component\",\n    key=key,\n    events={\n      \"decrementEvent\": on_decrement,\n    },\n    properties={\n      \"value\": value,\n    },\n  )\n

The first part you will notice is the decorator: @mel.web_component. This annotates a function as a web component and specifies where the corresponding JavaScript module is located, relative to the location of this Python module.

We've defined the function parameters just like a regular Python function.

Tip: We recommend annotating your parameter with types because Mesop will do runtime validation which will catch type issues earlier.

Finally, we call the function mel.insert_web_component with the following arguments:

  • name - This is the web component name and must match the name defined in the JavaScript module.
  • key - Like all components, web components accept a key which is a unique identifier. See the component key docs for more info.
  • events - A dictionary where the key is the event name. This must match a property name, defined in JavaScript. The value is the event handler (callback) function.
  • properties - A dictionary where the key is the property name that's defined in JavaScript and the value is the property value which is plumbed to the JavaScript component.

Note: Keys for events and properties must not be \"src\", \"srcdoc\", or start with \"on\" to avoid web security risks.

In summary, when you see a string literal, it should match something on the JavaScript side which is explained next.

"},{"location":"web-components/quickstart/#javascript-module","title":"JavaScript module","text":"

Let's now take a look at how we implement in the web component in JavaScript:

counter_component.js
import {\n  LitElement,\n  html,\n} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';\n\nclass CounterComponent extends LitElement {\n  static properties = {\n    value: {type: Number},\n    decrementEvent: {type: String},\n  };\n\n  constructor() {\n    super();\n    this.value = 0;\n    this.decrementEvent = '';\n  }\n\n  render() {\n    return html`\n      <div class=\"container\">\n        <span>Value: ${this.value}</span>\n        <button id=\"decrement-btn\" @click=\"${this._onDecrement}\">\n          Decrement\n        </button>\n      </div>\n    `;\n  }\n\n  _onDecrement() {\n    this.dispatchEvent(\n      new MesopEvent(this.decrementEvent, {\n        value: this.value - 1,\n      }),\n    );\n  }\n}\n\ncustomElements.define('quickstart-counter-component', CounterComponent);\n

In this example, we have used Lit which is a small library built on top of web standards in a simple, secure and declarative manner.

Note: you can write your web components using any web technologies (e.g. TypeScript) or frameworks as long as they conform to the interface defined by your Python module.

"},{"location":"web-components/quickstart/#properties","title":"Properties","text":"

The static property named properties defines two kinds of properties:

  • Regular properties - these were defined in the properties argument of insert_web_component. The property name in JS must match one of the properties dictionary key. You also should make sure the Python and JS types are compatible to avoid issues.
  • Event properties - these were defined in the events argument of insert_web_component. The property name in JS must match one of the events dictionary key. Event properties are always type String because the value is a handler id which identifies the Python event handler function.
"},{"location":"web-components/quickstart/#triggering-an-event","title":"Triggering an event","text":"

To trigger an event in your component, let's look at the _onDecrement method implementation:

this.dispatchEvent(\n  new MesopEvent(this.decrementEvent, {\n    value: this.value - 1,\n  }),\n);\n

this.dispatchEvent is a standard web API where a DOM element can emit an event. For Mesop web components, we will always emit a MesopEvent which is a class provided on the global object (window). The first argument is the event handler id so Mesop knows which Python function to call as the event handler and the second argument is the payload which is a JSON-serializable value (oftentimes an object) that the Python event handler can access.

"},{"location":"web-components/quickstart/#learn-more-about-lit","title":"Learn more about Lit","text":"

I didn't cover the render function which is a standard Lit method. I recommend reading through Lit's docs which are excellent ahd have interactive tutorials.

"},{"location":"web-components/quickstart/#using-the-component","title":"Using the component","text":"

Finally, let's use the web component we defined. When you click on the decrement button, the value will decrease from 10 to 9 and so on.

counter_component_app.py
from pydantic import BaseModel\n\nimport mesop as me\nimport mesop.labs as mel\nfrom mesop.examples.web_component.quickstart.counter_component import (\n  counter_component,\n)\n\n\n@me.page(\n  path=\"/web_component/quickstart/counter_component_app\",\n  security_policy=me.SecurityPolicy(\n    allowed_script_srcs=[\n      \"https://cdn.jsdelivr.net\",\n    ]\n  ),\n)\ndef page():\n  counter_component(\n    value=me.state(State).value,\n    on_decrement=on_decrement,\n  )\n\n\n@me.stateclass\nclass State:\n  value: int = 10\n\n\nclass ChangeValue(BaseModel):\n  value: int\n\n\ndef on_decrement(e: mel.WebEvent):\n  # Creating a Pydantic model from the JSON value of the WebEvent\n  # to enforce type safety.\n  decrement = ChangeValue(**e.value)\n  me.state(State).value = decrement.value\n

Even though this was a toy example, you've learned how to build a web component from scratch which does bi-directional communication between the Python server and JavaScript client.

"},{"location":"web-components/quickstart/#next-steps","title":"Next steps","text":"

To learn more, read the API docs or look at the examples.

"},{"location":"web-components/troubleshooting/","title":"Web Components Troubleshooting","text":""},{"location":"web-components/troubleshooting/#security-policy","title":"Security policy","text":"

One of the most common issues when using web components is that you will need to relax the stringent security policy Mesop uses by default.

If you use the mesop command-line tool to run your app, you will see a detailed error message printed like this that will tell you how to fix the error:

If you are using Colab or another tool to run your Mesop app, and you can't see the terminal messages, then you can use your browser developer tools to view the console error messages.

"},{"location":"web-components/troubleshooting/#content-security-policy-error-messages","title":"Content security policy error messages","text":"

In your browser developer tools, you may see the following console error messages (the exact wording may differ):

"},{"location":"web-components/troubleshooting/#script-src-error","title":"script-src Error","text":"script-src Console Error

Refused to load the script 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js' because it violates the following Content Security Policy directive: \"script-src 'self' 'nonce-X-_ZR64fycojGBCDQbjpLA'\". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

If you see this error message, then you will need to update your page's Security Policy allowed_script_srcs property. In this example, because the \"script-src\" directive was violated, you will need to add the script's URL to the Security Policy like this:

@me.page(\n    security_policy=me.SecurityPolicy(\n        allowed_script_srcs=[\"https://cdn.jsdelivr.net\"]\n    )\n)\n
Allow-listing sites

You can allow-list the full URL including the path, but it's usually more convenient to allow-list the entire site. This depends on how trustworthy the site is.

"},{"location":"web-components/troubleshooting/#connect-src-error","title":"connect-src Error","text":"connect-src Console Error

zone.umd.js:2767 Refused to connect to 'https://identitytoolkit.googleapis.com/v1/projects' because it violates the following Content Security Policy directive: \"default-src 'self'\". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

If you see this error message, then you will need to update your page's Security Policy allowed_connect_srcs property. In this example, because the \"connect-src\" directive was violated, you will need to add the URL you are trying to connect to (e.g. XHR, fetch) to the Security Policy like this:

@me.page(\n    security_policy=me.SecurityPolicy(\n        allowed_connect_srcs=[\"https://*.googleapis.com\"]\n    )\n)\n
Allow-listing domains using wildcard

You can wildcard all the subdomains for a site by using the wildcard character *.

"},{"location":"web-components/troubleshooting/#trusted-types-error","title":"Trusted Types Error","text":"

Trusted Types errors can come in various forms. If you see a console error message that contains TrustedHTML, TrustedScriptURL or some other variation, then you are likely hitting a trusted types error. Trusted Types is a powerful web security feature which prevents untrusted code from using sensitive browser APIs.

Unfortunately, many third-party libraries are incompatible with trusted types which means you need to disable this web security defense protection for the Mesop page which uses these libraries via web components.

TrustedHTML Console Error

TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.

You can fix this Trusted Types error by disabling Trusted Types in the security policy like this:

@me.page(\n    security_policy=me.SecurityPolicy(\n        dangerously_disable_trusted_types=True\n    )\n)\n
"},{"location":"blog/archive/2024/","title":"2024","text":""},{"location":"blog/archive/2023/","title":"2023","text":""}]} \ No newline at end of file diff --git a/showcase/index.html b/showcase/index.html new file mode 100644 index 000000000..1d12f682c --- /dev/null +++ b/showcase/index.html @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + Showcase 🌐 - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Showcase 🌐

+ + +
+

hide: + - navigation + - toc

+
+ +

+ + + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 000000000..0f8724efd --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 000000000..ba48cb4e4 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/stylesheets/demo.css b/stylesheets/demo.css new file mode 100644 index 000000000..3863fd6fe --- /dev/null +++ b/stylesheets/demo.css @@ -0,0 +1,10 @@ +header, +.md-container { + display: none !important; +} + +.full-screen-iframe { + border: 0; + width: 100vw; + height: 100vh; +} diff --git a/stylesheets/extra.css b/stylesheets/extra.css new file mode 100644 index 000000000..2144954cf --- /dev/null +++ b/stylesheets/extra.css @@ -0,0 +1,569 @@ +p { + letter-spacing: 0.07px; +} + +/* + * Forked from: https://github.com/mesop-sh/ruff/blob/39728a1198f1ed6081a0fd159715737848a50aa2/docs/stylesheets/extra.css#L32 + */ +:root { + --black: #0f0f11; + --almost-white: #e3e3e3; + --white: white; + --radiate: #d7ff64; + --flare: #6340ac; + --rock: #78876e; + --galaxy: #121230; + --space: #171414; + --comet: #6f5d6f; + --cosmic: #cb00dc; + --sun: #ffac2f; + --electron: #46ebe1; + --aurora: #46eb74; + --constellation: #5f6de9; + --neutron: #cff3cf; + --proton: #f6afbc; + --nebula: #cdcbfb; + --supernova: #f1aff6; + --starlight: #f4f4f1; + --lunar: #fbf2fc; + --asteroid: #e3cee3; + --crater: #f0dfdf; + --mesop-blue-40: #0064ff; + --mesop-blue-35: #0052ce; + /* Tailwind colors: https://tailwindcss.com/docs/customizing-colors#default-color-palette */ + --teal-600: #0d9488; + --indigo-600: #4f46e5; + --pink-600: #db2777; + --amber-600: #d97706; +} + +:root { + --primary-button-background-color: var(--mesop-blue-40); + --primary-button-background-color-hover: var(--mesop-blue-35); + --primary-button-text-color: var(--white); + --primary-button-text-color-hover: var(--almost-white); +} + +[data-md-color-scheme='mesop-light'] { + /* --md-default-bg-color: #fafafc; */ + --md-default-fg-color--light: #475569; + --md-default-bg-color--dark: var(--black); + --md-default-bg-color--lighter: var(--almost-white); + --md-primary-fg-color: rgb(248 250 252 / 1); + --md-primary-fg-color--dark: var(--md-primary-fg-color); + --md-default-bg-color: var(--md-primary-fg-color); + --md-primary-bg-color: #333; + --md-typeset-a-color: var(--cosmic); + --md-accent-fg-color: var(--flare); +} + +[data-md-color-scheme='mesop-dark'] { + --md-default-bg-color: var(--black); /* var(--galaxy);*/ + --md-default-fg-color: var(--almost-white); + --md-default-fg-color--light: var(--almost-white); + --md-default-fg-color--lighter: var(--almost-white); + --md-primary-fg-color: var(--black); /* var(--space); */ + --md-primary-bg-color: var(--almost-white); + --md-accent-fg-color: var(--cosmic); + + --md-typeset-color: var(--almost-white); + --md-typeset-a-color: var(--radiate); + --md-typeset-mark-color: var(--sun); + + --md-code-fg-color: var(--almost-white); + --md-code-bg-color: var(--space); + + --md-code-hl-comment-color: var(--asteroid); + --md-code-hl-punctuation-color: var(--asteroid); + --md-code-hl-generic-color: var(--supernova); + --md-code-hl-variable-color: var(--starlight); + --md-code-hl-string-color: var(--radiate); + --md-code-hl-keyword-color: var(--supernova); + --md-code-hl-operator-color: var(--supernova); + --md-code-hl-number-color: var(--electron); + --md-code-hl-special-color: var(--electron); + --md-code-hl-function-color: var(--neutron); + --md-code-hl-constant-color: var(--radiate); + --md-code-hl-name-color: var(--md-code-fg-color); + + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + --md-typeset-table-color: hsla(0, 0%, 100%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 100%, 0.035); +} + +[data-md-color-scheme='mesop-light'] img[src$='#only-dark'], +[data-md-color-scheme='mesop-light'] img[src$='#gh-dark-mode-only'] { + display: none; /* Hide dark images in light mode */ +} + +[data-md-color-scheme='mesop-light'] img[src$='#only-light'], +[data-md-color-scheme='mesop-light'] img[src$='#gh-light-mode-only'] { + display: inline; /* Show light images in light mode */ +} + +[data-md-color-scheme='mesop-dark'] img[src$='#only-light'], +[data-md-color-scheme='mesop-dark'] img[src$='#gh-light-mode-only'] { + display: none; /* Hide light images in dark mode */ +} + +[data-md-color-scheme='mesop-dark'] img[src$='#only-dark'], +[data-md-color-scheme='mesop-dark'] img[src$='#gh-dark-mode-only'] { + display: inline; /* Show dark images in dark mode */ +} + +.md-header--shadow { + box-shadow: none; +} + +[data-md-color-scheme='mesop-light'] .md-header__topic { + /* color: #444; TODO */ + font-weight: 600; +} + +/* See: https://github.com/squidfunk/mkdocs-material/issues/175#issuecomment-616694465 */ +.md-typeset__table { + min-width: 100%; +} +.md-typeset table:not([class]) { + display: table; +} + +/* See: https://github.com/mesop-sh/ruff/issues/8519 */ +[data-md-color-scheme='mesop-dark'] details summary a { + color: var(--flare); +} + +.md-nav--primary { + font-size: 0.8rem; + font-weight: 500; +} + +.md-nav--secondary { + font-size: 0.75rem; +} + +.md-main__inner { + margin-top: 0.5rem; +} + +.md-nav--primary a.md-nav__link { + color: #444; + font-size: 0.8rem; + letter-spacing: 0.14px; +} + +[data-md-color-scheme='mesop-dark'] .md-nav--primary a.md-nav__link { + color: white; +} + +.md-typeset { + font-size: 0.8rem; +} + +.md-typeset .admonition, +.md-typeset details { + font-size: 0.8rem; +} + +.highlight code { + font-size: 0.7rem; +} + +.md-tabs__link { + font-size: 0.75rem; + font-weight: 600; +} + +.md-search__input, +.md-search__form { + border-radius: 8px; +} + +.md-search__input { + background-color: var(--md-default-bg-color--lighter); +} + +.md-search__input::placeholder { + color: var(--md-default-bg-color--dark); +} + +/* Based on https://github.com/squidfunk/mkdocs-material/issues/2574#issuecomment-821979698 */ +[data-md-toggle='search']:not(:checked) ~ .md-header .md-search__form::after { + position: absolute; + top: 0.3rem; + right: 0.5rem; + display: block; + padding: 0.1rem 0.4rem; + background: var(--md-default-bg-color); + color: var(--md-default-bg-color--dark); + font-weight: bold; + font-size: 0.8rem; + border: 0.05rem solid var(--md-default-bg-color--lighter); + box-shadow: 0 1px 2px var(--md-default-bg-color--lighter); + text-shadow: 0 1px 0 var(--md-default-bg-color--lighter); + border-radius: 0.2rem; + z-index: 9; + content: '⌘ K'; +} + +[data-md-color-scheme='mesop-dark'] .md-logo { + filter: invert(1); +} + +.immersive-demo { + width: 100%; + height: 600px; + overflow-y: auto; + border: 0; +} + +.component-demo { + width: 100%; + height: 400px; + overflow-y: auto; + border: 1px solid #dcdcdc; +} + +/* Caps the height of the code snippets. */ +.md-typeset pre > code { + max-height: 450px; +} + +/* Used for home page */ + +.headline { + margin-top: 36px; + margin-bottom: 24px; + font-size: 48px; + font-weight: 800; + text-align: center; + line-height: 1.1; +} + +.sub-headline { + font-size: 18px; + text-align: center; + color: var(--md-default-fg-color--light); +} + +.header-buttons { + margin-top: 24px; + display: flex; + justify-content: center; + align-items: center; + gap: 16px; +} + +a.primary-button { + background: var(--primary-button-background-color); + color: var(--primary-button-text-color); + font-weight: 600; + padding: 12px 16px; + border-radius: 12px; + font-size: 14px; +} + +a.primary-button:hover { + background: var(--primary-button-background-color-hover); + color: var(--primary-button-text-color-hover); +} + +a.secondary-button { + border: 1px solid var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color); + font-weight: 600; + padding: 12px 16px; + border-radius: 12px; + font-size: 14px; +} + +a.secondary-button:hover { + color: var(--md-default-fg-color--light); + background: var(--md-default-bg-color--lighter); +} + +.section-headline { + font-size: 36px; + font-weight: 800; + line-height: 1.1; +} + +.box-shadow { + --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), + 0 4px 6px -4px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), + 0 4px 6px -4px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), + var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + +.main-demo-iframe-container { + position: relative; + width: 80%; + margin-top: 24px; +} + +.main-demo-iframe { + width: 100%; + border-radius: 8px; + height: 500px; + border: 0; +} + +.main-demo-iframe-container .embed-overlay { + position: absolute; + top: 8px; + right: 48px; +} + +.main-demo-iframe-container .open-button { + background-color: var(--primary-button-background-color); + color: white; + padding: 12px 16px; + border: none; + border-radius: 20px; + cursor: pointer; + font-size: 14px; + font-weight: bold; + transition: all 0.3s ease; + display: flex; + align-items: center; + gap: 6px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} + +.main-demo-iframe-container .open-button:hover { + background-color: var(--primary-button-background-color-hover); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.main-demo-iframe-container .open-button svg { + width: 24px; + height: 24px; + transition: transform 0.3s ease; +} + +.main-demo-iframe-container .open-button:hover svg { + transform: translateX(2px); +} + +.chat-inputs-iframe { + margin-top: 24px; + width: min(100%, 760px); + border-radius: 8px; + height: 450px; + border: 0; +} + +.center { + display: flex; + justify-content: center; + align-items: center; +} + +.column { + flex-direction: column; +} + +.container { + padding-top: 36px; + padding-bottom: 48px; +} + +[data-md-color-scheme='mesop-dark'] .container { + background: var(--md-default-bg-color); +} + +.amber-gradient-background { + --a: #fff8e0; + --b: #fff9e5; + --c: #fffaf0; + background: radial-gradient( + circle farthest-side at 28% 54%, + var(--a), + transparent 36% + ), + radial-gradient(circle farthest-side at 65% 45%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 65%, var(--c), transparent 50%); +} + +.blue-gradient-background { + --a: #eaf4ff; + --b: #eef2ff; + --c: #f0f2ff; + background: radial-gradient( + circle farthest-side at 28% 54%, + var(--a), + transparent 36% + ), + radial-gradient(circle farthest-side at 65% 45%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 65%, var(--c), transparent 50%); +} + +.pink-gradient-background { + --a: #fde7f0; + --b: #ffeef5; + --c: #fff7fb; + background: radial-gradient( + circle farthest-side at 28% 54%, + var(--a), + transparent 36% + ), + radial-gradient(circle farthest-side at 65% 45%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 65%, var(--c), transparent 50%); +} + +.red-gradient-background { + --a: #ffd6d6; + --b: #ffe5e5; + --c: #ffeded; + background: radial-gradient( + circle farthest-side at 28% 54%, + var(--a), + transparent 36% + ), + radial-gradient(circle farthest-side at 65% 45%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 65%, var(--c), transparent 50%); +} + +.teal-gradient-background { + --a: #fbfdfe; + --b: #e5fbff; + --c: #ddfaff; + background: radial-gradient( + circle farthest-side at 30% 50%, + var(--a), + transparent 40% + ), + radial-gradient(circle farthest-side at 70% 40%, var(--b), transparent 50%), + radial-gradient(circle farthest-side at 60% 60%, var(--c), transparent 50%); +} + +.section-chip { + font-weight: 600; + margin-bottom: 12px; +} + +.section-chip.teal { + color: var(--teal-600); +} + +.section-chip.blue { + color: var(--mesop-blue-40); +} + +.section-chip.pink { + color: var(--pink-600); +} + +.section-chip.indigo { + color: var(--indigo-600); +} + +.section-chip.amber { + color: var(--amber-600); +} + +.section-body { + padding: 12px 0; + max-width: 768px; +} + +a.section-button { + display: block; + background: var(--md-default-bg-color); + margin: 16px 0; + border: 1px solid var(--md-default-fg-color--lightest); + color: var(--md-default-fg-color); + font-weight: 600; + padding: 16px; + border-radius: 12px; + max-width: 480px; +} + +a.section-button:hover { + background: var(--md-default-fg-color--lightest); +} + +.section-button-subtext { + font-weight: normal; +} + +.youtube-embed { + margin-top: 12px; + border-radius: 12px; +} + +.twitter-row { + display: flex; + gap: 16px; + margin-top: 8px; +} + +@media (max-width: 760px) { + .twitter-row { + flex-direction: column; + gap: 12px; + } +} + +.gif-caption { + font-size: 24px; + padding: 24px; + text-align: center; +} + +.dev-gif-row { + margin-top: 36px; + margin-bottom: 48px; + display: flex; + justify-content: space-between; + align-items: center; + max-width: 840px; +} + +.row-reverse { + flex-direction: row-reverse; +} + +@media (max-width: 760px) { + .dev-gif-row { + flex-direction: column; + } +} + +.dev-gif { + width: 560px; + border-radius: 16px; +} + +/* Styles for various docs page */ + +a.next-step { + display: block; + position: relative; + margin-top: 16px; + border: 1px solid var(--md-default-bg-color--lighter); + + padding: 32px 16px 16px; + border-radius: 8px; + width: 400px; +} + +a.next-step:before { + content: 'Next'; + color: var(--md-typeset-color); + font-size: 14px; + position: absolute; + top: 8px; +} + +a.next-step:hover { + border: 1px solid var(--md-accent-fg-color); +} + +h3#mesop\.features\.theme\.ThemeVar { + font-size: 0.8rem; +} diff --git a/theme/main.html b/theme/main.html new file mode 100644 index 000000000..d8b66ddb1 --- /dev/null +++ b/theme/main.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} {% block announce %} Please +upgrade to the latest +version of Mesop which addresses an important +security issue.{% endblock %} diff --git a/theme/partials/header.html b/theme/partials/header.html new file mode 100644 index 000000000..ddbac2eeb --- /dev/null +++ b/theme/partials/header.html @@ -0,0 +1,99 @@ + + + + +{% set class = "md-header" %} {% if "navigation.tabs.sticky" in features %} {% +set class = class ~ " md-header--shadow md-header--lifted" %} {% elif +"navigation.tabs" not in features %} {% set class = class ~ " md-header--shadow" +%} {% endif %} + + +
+ + + + {% if "navigation.tabs.sticky" in features %} {% if "navigation.tabs" in + features %} {% include "partials/tabs.html" %} {% endif %} {% endif %} +
diff --git a/theme/partials/search.html b/theme/partials/search.html new file mode 100644 index 000000000..a5f86bb22 --- /dev/null +++ b/theme/partials/search.html @@ -0,0 +1,67 @@ + + diff --git a/web-components/api/index.html b/web-components/api/index.html new file mode 100644 index 000000000..c2f2c2b33 --- /dev/null +++ b/web-components/api/index.html @@ -0,0 +1,2797 @@ + + + + + + + + + + + + + + + + + + + + + + + API - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Web Components API

+
+

Note: Web components are a new experimental feature released under labs and may have breaking changes.

+
+

Example usage:

+
import mesop.labs as mel
+
+
+@mel.web_component(...)
+def a_web_component():
+    mel.insert_web_component(...)
+
+

API

+ + +
+ + +

+ web_component + +

+ + +
+ +

A decorator for defining a web component.

+

This decorator is used to define a web component. It takes a path to the +JavaScript file of the web component and an optional parameter to skip +validation. It then registers the JavaScript file in the runtime.

+ + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
path +
+

The path to the JavaScript file of the web component.

+
+

+ + TYPE: + str + +

+
skip_validation +
+

If set to True, skips validation. Defaults to False.

+
+

+ + TYPE: + bool + + + DEFAULT: + False + +

+
+ +
+ +
+ +
+ + +

+ insert_web_component + +

+ + +
+ +

Inserts a web component into the current component tree.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
PARAMETERDESCRIPTION
name +
+

The name of the web component. This should match the custom element name defined in JavaScript.

+
+

+ + TYPE: + str + +

+
events +
+

A dictionary where the key is the event name, which must match a web component property name defined in JavaScript. + The value is the event handler (callback) function. + Keys must not be "src", "srcdoc", or start with "on" to avoid web security risks.

+
+

+ + TYPE: + dict[str, Callable[[WebEvent], Any]] | None + + + DEFAULT: + None + +

+
properties +
+

A dictionary where the key is the web component property name that's defined in JavaScript and the value is the + property value which is plumbed to the JavaScript component. + Keys must not be "src", "srcdoc", or start with "on" to avoid web security risks.

+
+

+ + TYPE: + dict[str, Any] | None + + + DEFAULT: + None + +

+
key +
+

A unique identifier for the web component. Defaults to None.

+
+

+ + TYPE: + str | None + + + DEFAULT: + None + +

+
+ +
+ +
+ +
+ + + +

+ WebEvent + + + dataclass + + +

+ + +
+

+ Bases: MesopEvent

+ + +

An event emitted by a web component.

+ + + + + + + + + + + + + + + + + + + +
ATTRIBUTEDESCRIPTION
value +
+

The value associated with the web event.

+
+

+ + TYPE: + Any + +

+
key +
+

key of the component that emitted this event.

+
+

+ + TYPE: + str + +

+
+ + + + +
+ + + + + + + + + + + +
+ +
+ +
+ +
+ + +

+ slot + +

+ + +
+ +

This function is used when defining a content component to mark a place in the component tree where content +can be provided by a child component.

+ +
+ +
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/web-components/index.html b/web-components/index.html new file mode 100644 index 000000000..4a3ecddba --- /dev/null +++ b/web-components/index.html @@ -0,0 +1,2495 @@ + + + + + + + + + + + + + + + + + + + + + + + Overview - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Web Components

+
+

Note: Web components are a new experimental feature released under labs and may have breaking changes.

+
+

Mesop allows you to define custom components with web components which is a set of web standards that allows you to use JavaScript and CSS to define custom HTML elements.

+

Use cases

+
    +
  • +

    Custom JavaScript - You can execute custom JavaScript and have simple bi-directional communication between the JavaScript code running in the browser and the Python code running the server.

    +
  • +
  • +

    JavaScript libraries - If you want to use a JavaScript library, you can wrap them with a web component.

    +
  • +
  • +

    Rich-client side interactivity - You can use web components to deliver stateful client-side interactions without a network roundtrip.

    +
  • +
+

Anatomy of a web component

+

Mesop web component consists of two parts:

+
    +
  • Python module - defines a Python API so that your Mesop app can use the web component seamlessly.
  • +
  • JavaScript module - implements the web component.
  • +
+

Next steps

+

Learn how to build your first web component in the quickstart page.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/web-components/quickstart/index.html b/web-components/quickstart/index.html new file mode 100644 index 000000000..3e0a1c0d0 --- /dev/null +++ b/web-components/quickstart/index.html @@ -0,0 +1,2707 @@ + + + + + + + + + + + + + + + + + + + + + + + Quickstart - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Quickstart

+
+

Note: Web components are a new experimental feature released under labs and may have breaking changes.

+
+

You will learn how to build your first web component step-by-step, a counter component.

+

Although it's a simple example, it will show you the core APIs of defining your own web component and how to support bi-directional communication between the Python code running on the server and JavaScript code running on the browser.

+

Python module

+

Let's first take a look at the Python module which defines the interface so that the rest of your Mesop app can call the web component in a Pythonic way.

+
counter_component.py
from typing import Any, Callable
+
+import mesop.labs as mel
+
+
+@mel.web_component(path="./counter_component.js")
+def counter_component(
+  *,
+  value: int,
+  on_decrement: Callable[[mel.WebEvent], Any],
+  key: str | None = None,
+):
+  return mel.insert_web_component(
+    name="quickstart-counter-component",
+    key=key,
+    events={
+      "decrementEvent": on_decrement,
+    },
+    properties={
+      "value": value,
+    },
+  )
+
+

The first part you will notice is the decorator: @mel.web_component. This annotates a function as a web component and specifies where the corresponding JavaScript module is located, relative to the location of this Python module.

+

We've defined the function parameters just like a regular Python function.

+
+

Tip: We recommend annotating your parameter with types because Mesop will do runtime validation which will catch type issues earlier.

+
+

Finally, we call the function mel.insert_web_component with the following arguments:

+
    +
  • name - This is the web component name and must match the name defined in the JavaScript module.
  • +
  • key - Like all components, web components accept a key which is a unique identifier. See the component key docs for more info.
  • +
  • events - A dictionary where the key is the event name. This must match a property name, defined in JavaScript. The value is the event handler (callback) function.
  • +
  • properties - A dictionary where the key is the property name that's defined in JavaScript and the value is the property value which is plumbed to the JavaScript component.
  • +
+
+

Note: Keys for events and properties must not be "src", "srcdoc", or start with "on" to avoid web security risks.

+
+

In summary, when you see a string literal, it should match something on the JavaScript side which is explained next.

+

JavaScript module

+

Let's now take a look at how we implement in the web component in JavaScript:

+
counter_component.js
import {
+  LitElement,
+  html,
+} from 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js';
+
+class CounterComponent extends LitElement {
+  static properties = {
+    value: {type: Number},
+    decrementEvent: {type: String},
+  };
+
+  constructor() {
+    super();
+    this.value = 0;
+    this.decrementEvent = '';
+  }
+
+  render() {
+    return html`
+      <div class="container">
+        <span>Value: ${this.value}</span>
+        <button id="decrement-btn" @click="${this._onDecrement}">
+          Decrement
+        </button>
+      </div>
+    `;
+  }
+
+  _onDecrement() {
+    this.dispatchEvent(
+      new MesopEvent(this.decrementEvent, {
+        value: this.value - 1,
+      }),
+    );
+  }
+}
+
+customElements.define('quickstart-counter-component', CounterComponent);
+
+

In this example, we have used Lit which is a small library built on top of web standards in a simple, secure and declarative manner.

+
+

Note: you can write your web components using any web technologies (e.g. TypeScript) or frameworks as long as they conform to the interface defined by your Python module.

+
+

Properties

+

The static property named properties defines two kinds of properties:

+
    +
  • Regular properties - these were defined in the properties argument of insert_web_component. The property name in JS must match one of the properties dictionary key. You also should make sure the Python and JS types are compatible to avoid issues.
  • +
  • Event properties - these were defined in the events argument of insert_web_component. The property name in JS must match one of the events dictionary key. Event properties are always type String because the value is a handler id which identifies the Python event handler function.
  • +
+

Triggering an event

+

To trigger an event in your component, let's look at the _onDecrement method implementation:

+
this.dispatchEvent(
+  new MesopEvent(this.decrementEvent, {
+    value: this.value - 1,
+  }),
+);
+
+

this.dispatchEvent is a standard web API where a DOM element can emit an event. For Mesop web components, we will always emit a MesopEvent which is a class provided on the global object (window). The first argument is the event handler id so Mesop knows which Python function to call as the event handler and the second argument is the payload which is a JSON-serializable value (oftentimes an object) that the Python event handler can access.

+

Learn more about Lit

+

I didn't cover the render function which is a standard Lit method. I recommend reading through Lit's docs which are excellent ahd have interactive tutorials.

+

Using the component

+

Finally, let's use the web component we defined. When you click on the decrement button, the value will decrease from 10 to 9 and so on.

+
counter_component_app.py
from pydantic import BaseModel
+
+import mesop as me
+import mesop.labs as mel
+from mesop.examples.web_component.quickstart.counter_component import (
+  counter_component,
+)
+
+
+@me.page(
+  path="/web_component/quickstart/counter_component_app",
+  security_policy=me.SecurityPolicy(
+    allowed_script_srcs=[
+      "https://cdn.jsdelivr.net",
+    ]
+  ),
+)
+def page():
+  counter_component(
+    value=me.state(State).value,
+    on_decrement=on_decrement,
+  )
+
+
+@me.stateclass
+class State:
+  value: int = 10
+
+
+class ChangeValue(BaseModel):
+  value: int
+
+
+def on_decrement(e: mel.WebEvent):
+  # Creating a Pydantic model from the JSON value of the WebEvent
+  # to enforce type safety.
+  decrement = ChangeValue(**e.value)
+  me.state(State).value = decrement.value
+
+

Even though this was a toy example, you've learned how to build a web component from scratch which does bi-directional communication between the Python server and JavaScript client.

+

Next steps

+

To learn more, read the API docs or look at the examples.

+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/web-components/troubleshooting/index.html b/web-components/troubleshooting/index.html new file mode 100644 index 000000000..8fd8c418f --- /dev/null +++ b/web-components/troubleshooting/index.html @@ -0,0 +1,2585 @@ + + + + + + + + + + + + + + + + + + + + + + + Troubleshooting - Mesop + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + +
+ + + + + +
+ +
+ + + + +
+
+ + + +
+
+
+ + + + + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + +

Web Components Troubleshooting

+

Security policy

+

One of the most common issues when using web components is that you will need to relax the stringent security policy Mesop uses by default.

+

If you use the mesop command-line tool to run your app, you will see a detailed error message printed like this that will tell you how to fix the error:

+

+

If you are using Colab or another tool to run your Mesop app, and you can't see the terminal messages, then you can use your browser developer tools to view the console error messages.

+

Content security policy error messages

+

In your browser developer tools, you may see the following console error messages (the exact wording may differ):

+

script-src Error

+
+script-src Console Error +

Refused to load the script 'https://cdn.jsdelivr.net/gh/lit/dist@3/core/lit-core.min.js' because it violates the following Content Security Policy directive: "script-src 'self' 'nonce-X-_ZR64fycojGBCDQbjpLA'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

+
+

If you see this error message, then you will need to update your page's Security Policy allowed_script_srcs property. In this example, because the "script-src" directive was violated, you will need to add the script's URL to the Security Policy like this:

+
@me.page(
+    security_policy=me.SecurityPolicy(
+        allowed_script_srcs=["https://cdn.jsdelivr.net"]
+    )
+)
+
+
+Allow-listing sites +

You can allow-list the full URL including the path, but it's usually more convenient + to allow-list the entire site. This depends on how trustworthy the site is.

+
+

connect-src Error

+
+connect-src Console Error +

zone.umd.js:2767 Refused to connect to 'https://identitytoolkit.googleapis.com/v1/projects' because it violates the following Content Security Policy directive: "default-src 'self'". Note that 'connect-src' was not explicitly set, so 'default-src' is used as a fallback.

+
+

If you see this error message, then you will need to update your page's Security Policy allowed_connect_srcs property. In this example, because the "connect-src" directive was violated, you will need to add the URL you are trying to connect to (e.g. XHR, fetch) to the Security Policy like this:

+
@me.page(
+    security_policy=me.SecurityPolicy(
+        allowed_connect_srcs=["https://*.googleapis.com"]
+    )
+)
+
+
+Allow-listing domains using wildcard +

You can wildcard all the subdomains for a site by using the wildcard character *.

+
+

Trusted Types Error

+

Trusted Types errors can come in various forms. If you see a console error message that contains TrustedHTML, TrustedScriptURL or some other variation, then you are likely hitting a trusted types error. Trusted Types is a powerful web security feature which prevents untrusted code from using sensitive browser APIs.

+

Unfortunately, many third-party libraries are incompatible with trusted types which means you need to disable this web security defense protection for the Mesop page which uses these libraries via web components.

+
+TrustedHTML Console Error +

TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.

+
+

You can fix this Trusted Types error by disabling Trusted Types in the security policy like this:

+
@me.page(
+    security_policy=me.SecurityPolicy(
+        dangerously_disable_trusted_types=True
+    )
+)
+
+ + + + + + + + + + + + + +
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + + + + + + + + \ No newline at end of file

The demo gallery itself is a Mesop app and implemented in a few hundred lines of Python code. It demonstrates how Mesop can be used to create polished, custom UIs in a maintainable way.