diff --git a/examples/apps/dashboard/dashboard.py b/examples/apps/dashboard/dashboard.py
new file mode 100644
index 00000000..bdd6571d
--- /dev/null
+++ b/examples/apps/dashboard/dashboard.py
@@ -0,0 +1,223 @@
+# Inspiration: https://demos.creative-tim.com/material-dashboard/pages/dashboard
+# docs: https://holoviz-dev.github.io/panel-material-ui/
+# panel serve examples/apps/dashboard/*.py --dev
+import panel_material_ui as pmu
+import panel as pn
+import param
+import pandas as pd
+
+from panel_material_ui import ChangeIndicator
+from panel_material_ui import Icon
+
+from shared.data import get_project_data
+from shared.plots import (
+ get_website_views_config,
+ get_daily_sales_config,
+ get_completed_tasks_config,
+)
+from shared.components import create_menu, Timeline
+from shared.page import create_page
+
+my_theme = {
+ "palette": {
+ "primary": {
+ "main": "#FF5733",
+ # light, dark, and contrastText can be automatically computed
+ },
+ "secondary": {
+ "main": "#E0C2FF",
+ "light": "#F5EBFF", # optional
+ "dark": "#BA99D5", # optional
+ "contrastText": "#47008F", # optional
+ }
+ }
+}
+
+
+pn.extension("echarts")
+
+page_title = f"""\
+## Dashboard
+
+Check the sales, value and bounce rate by country.\
+"""
+
+indicators = pn.Row(
+ ChangeIndicator(
+ title="Today's Money", icon="weekend", value="$53,000", change_percent=55, since="since yesterday", sizing_mode="stretch_width"
+ ),
+ ChangeIndicator(title="Today's Users", icon="person", value="2300", change_percent=3, since="than last month", sizing_mode="stretch_width"),
+ ChangeIndicator(title="Ads Views", icon="leaderboard", value="3,462", change_percent=-2, since="since yesterday", sizing_mode="stretch_width"),
+ ChangeIndicator(title="Sales", icon="weekend", value="$103,430", change_percent=5, since="since yesterday", sizing_mode="stretch_width"),
+)
+
+
+def last_update(message):
+ schedule = Icon(value="schedule", font_size="small", sizing_mode="fixed", width=20, height=25, margin=(10,5, 10, 5))
+ return pn.Row(schedule, pn.pane.HTML(message, sizing_mode="fixed", margin=(11,0)), sizing_mode="fixed")
+
+def generate_company_html(row, image_width='40px'):
+ company_name=row["Company"]
+ company_image_url=row["CompanyImage"]
+ html = f'''
+
+

+
{company_name}
+
+ '''
+ return html
+
+def generate_progress_bar(row):
+ completion_percent = int(round(float(row["Completion"]),0))
+ color = 'green' if completion_percent >=99.999 else 'blue'
+ html = f'''
+
+
{completion_percent}%
+
+
+ '''
+ return html
+
+def generate_member_images(row, image_size='30px', overlap_offset='-10px'):
+ members = row["Members"]
+ images_html = ''
+ for idx, member in enumerate(members):
+ images_html += f'''
+
+ '''
+ wrapper_html = f'''
+
+ {images_html}
+
+ '''
+ return wrapper_html
+
+def to_styled_projects_table(data: pd.DataFrame):
+ data["Company"] = data.apply(generate_company_html, axis=1)
+ data["Completion"] = data.apply(generate_progress_bar, axis=1)
+ data["Members"] = data.apply(generate_member_images, axis=1)
+ data = data.drop(columns=["CompanyImage"])
+ styled_df = data.style
+ styled_df = (
+ styled_df.hide(axis="index")
+ .set_table_styles(
+ [
+ {
+ "selector": "th",
+ "props": [
+ ("background-color", "white"),
+ ("font-weight", "bold"),
+ ("border-bottom", "1px solid black"),
+ ("text-align", "left"),
+ ],
+ },
+ {
+ "selector": "td",
+ "props": [
+ ("padding", "10px"),
+ ("background", "white"),
+ ("border-top", "1px solid black"),
+ ],
+ },
+ ]
+ )
+ .set_properties(
+ **{"text-align": "left"},
+ )
+ )
+
+ return styled_df
+
+
+with pn.config.set(sizing_mode="stretch_width"):
+ web_site_views = pmu.Paper(
+ "#### Website Views\n\nLast Campaign Performance",
+ pn.pane.ECharts(
+ get_website_views_config(),
+ height=250,
+ margin=(0, 5),
+ ),
+ pn.Spacer(sizing_mode="stretch_height"),
+ pmu.Divider(margin=(10, 10, -5, 10)),
+ last_update("campaign sent 2 days ago"),
+ height=400, margin=10
+ )
+
+ daily_sales = pmu.Paper(
+ "#### Daily Sales\n\n**+15%** increase in todays sales",
+ pn.pane.ECharts(
+ get_daily_sales_config(),
+ height=250,
+ margin=(0, 5),
+ ),
+ pn.Spacer(sizing_mode="stretch_height"),
+ pmu.Divider(margin=(10,10,-5,10)),
+ last_update("updated 4 min ago"),
+ margin=10,
+ height=400,
+ )
+ completed_tasks = pmu.Paper(
+ "#### Completed Tasks\n\nLast Campaign Performance",
+ pn.pane.ECharts(
+ get_completed_tasks_config(),
+ height=250,
+ margin=(0, 5),
+ ),
+ pn.Spacer(sizing_mode="stretch_height"),
+ pmu.Divider(margin=(10,10,-5,10)),
+ last_update("just updated"),
+ margin=10,
+ height=400,
+ )
+
+ plots = pn.Row(web_site_views, daily_sales, completed_tasks, sizing_mode="stretch_both")
+ project_table = pmu.Paper(
+ "#### Projects\n\n**30 done** this month",
+ pn.pane.DataFrame(to_styled_projects_table(get_project_data()), sizing_mode="stretch_width"),
+ margin=10,
+ height=550,
+ )
+ timeline_config = [
+ {"content_title": "$2400, Design changes", "content": "22 DEC 7:20 PM", "color": "success", "icon": "notifications", "disable_dot": True},
+ {"content_title": "New order #1832412", "content": "21 DEC 11 PM", "color": "error", "icon": "code", "disable_dot": True},
+ {"content_title": "Server payments for April", "content": "21 DEC 9:34 PM", "color": "primary", "icon": "shopping_cart", "disable_dot": True},
+ {"content_title": "New card added for order #4395133", "content": "20 DEC 2:20 AM", "color": "warning", "icon": "credit_card", "disable_dot": True},
+ {"content_title": "Unlock packages for development", "content": "18 DEC 4:54 AM", "color": "error", "icon": "key", "disable_dot": True},
+ {"content_title": "New order #9583120", "content": "17 DEC", "color": "dark", "icon": "payments", "disable_dot": True},
+ ]
+ sx = {
+ "& .MuiTimelineItem-root:before": {
+ "flex": 0,
+ "padding-left": 10,
+ },
+ }
+ timeline_component = Timeline(object=timeline_config, sizing_mode="stretch_width", sx=sx)
+ timeline_component = pmu.Alert("The `Timeline` component does not work yet due to [#190](https://github.com/panel-extensions/panel-material-ui/issues/190).", alert_type="error", margin=10)
+ timeline = pmu.Paper(
+ "#### Orders overview\n\n**24%** this month",
+ timeline_component,
+ margin=10,
+ height=550,
+ )
+ table_timeline_row = pn.Row(project_table, timeline)
+
+create_page(
+ name="Dashboard",
+ main=[page_title, indicators, plots, table_timeline_row]).servable(
+ title="Dashboard"
+)
diff --git a/examples/apps/dashboard/notifications.py b/examples/apps/dashboard/notifications.py
new file mode 100644
index 00000000..aad8a096
--- /dev/null
+++ b/examples/apps/dashboard/notifications.py
@@ -0,0 +1,29 @@
+import panel as pn
+from shared.page import create_page
+from panel_material_ui import Button, Alert
+pn.extension(notifications=True)
+
+with pn.config.set(sizing_mode="stretch_width"):
+ alert_card = pn.Column(
+ """## Alerts
+
+Notifications on this page use the `Alert` from Material UI. Read more details [here](https://mui.com/material-ui/react-alert/).
+""",
+ *[Alert(title=f"A simple {severity} alert with an example link. Give it a click if you like.", severity=severity, variant="filled", closeable=True, margin=10) for severity in Alert.param.severity.objects],
+ max_width=1200,
+ )
+
+ success_notification = pn.widgets.Button(name="Success")
+ notifications_row = pn.Column(
+ """## Notifications
+
+Notifications on this page use the `SnackbarProvider` from notistack. Read more details [here](https://notistack.com/getting-started).""",
+ pn.Row(
+ Button(name="Error", color="error", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.error('This is an error notification.', duration=1000)),
+ Button(name="Info", color="info", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.info('This is an info notification.', duration=1000)),
+ Button(name="Success", color="success", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.success('This is a success notification.', duration=1000)),
+ Button(name="Warning", color="warning", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.warning('This is a warning notification.', duration=1000)),
+ )
+ )
+
+create_page("Notifications", main=[alert_card, notifications_row]).servable(title="Notifications")
diff --git a/examples/apps/dashboard/shared/Timeline.jsx b/examples/apps/dashboard/shared/Timeline.jsx
new file mode 100644
index 00000000..96ffd477
--- /dev/null
+++ b/examples/apps/dashboard/shared/Timeline.jsx
@@ -0,0 +1,59 @@
+import {
+ Timeline as MUITimeline,
+ TimelineItem,
+ TimelineSeparator,
+ TimelineConnector,
+ TimelineContent,
+ TimelineOppositeContent,
+ TimelineDot,
+} from "@mui/lab";
+import {Icon} from "@mui/material"
+import Typography from "@mui/material/Typography";
+
+export function render({model}) {
+ const [items] = model.useState("object")
+ const [position] = model.useState("position")
+ const [sx] = model.useState("sx")
+
+ return (
+
+ {items.map((item, idx) => (
+
+ {(item.opposite !== undefined || item.opposite_title !== undefined) && (
+
+
+ {item.opposite_title}
+
+
+ {item.opposite}
+
+
+ )}
+
+ {(item.disable_dot && item.icon) ? (
+ {item.icon}
+ ) : (
+
+ {item.icon !== undefined && (
+ {item.icon}
+ )}
+
+ )}
+ {idx < items.length - 1 && }
+
+ {(item.content !== undefined || item.content_title !== undefined) && (
+
+
+ {item.content_title}
+
+ {item.content}
+
+ )}
+
+ ))}
+
+ )
+}
diff --git a/examples/apps/dashboard/shared/__init__.py b/examples/apps/dashboard/shared/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/examples/apps/dashboard/shared/components.py b/examples/apps/dashboard/shared/components.py
new file mode 100644
index 00000000..62169fc1
--- /dev/null
+++ b/examples/apps/dashboard/shared/components.py
@@ -0,0 +1,78 @@
+import panel as pn
+import panel_material_ui as pmu
+from .config import PAGES
+import param
+import copy
+
+def create_menu(selected: str, pages: list[tuple] = PAGES, button_color="primary"):
+ active = -1
+ for index, page in enumerate(pages):
+ if selected==page["label"]:
+ active=index
+
+ def get_pages(button_color):
+ pages2 = copy.deepcopy(pages)
+ for item in pages2:
+ item["color"]=button_color
+ return pages2
+ return pmu.List(items=pn.bind(get_pages, button_color), active=active, margin=(0,10), sizing_mode="stretch_width")
+
+def create_card_with_jumbo_header(title: str, content: str):
+ return pmu.Paper(
+ pn.pane.Markdown(
+ title,
+ sizing_mode="stretch_width",
+ styles={
+ "background": "black",
+ "color": "white",
+ "border-radius": "5px",
+ "position": "relative",
+ "padding-left": "10px",
+ },
+ margin=10,
+ ),
+ content,
+ margin=10,
+ )
+
+class Timeline(pmu.MaterialUIComponent):
+ """Material‑UI **Timeline** component for Panel.
+
+ References:
+
+ - https://mui.com/material-ui/react-timeline/
+
+ Example:
+
+ >>> config = [
+ ... {"content_title": "Eat", "content": "Because you need strength", "opposite": "08:30", "color": "grey", "variant": "filled", "icon": "fastfood"},
+ ... {"content_title": "Code", "content": "Because it's awesome!", "opposite": "09:00", "color": "primary", "variant": "filled", "icon": "laptop_mac"},
+ ... {"content_title": "Sleep", "content": "Because you need rest", "opposite": "09:30", "color": "secondary", "variant": "outlined", "icon": "hotel"},
+ ... {"content_title": "Repeat", "content": "Because this is the life you love!", "opposite": "11:00", "color": "success", "variant": "filled", "icon": "repeat"},
+ ... ]
+ >>> Timeline(object=config, width=600)
+ """ # noqa: E501
+ object = param.List(default=[], doc="""
+ A list of dictionaries, each mapping directly onto a `TimelineItem` row.
+ Supported keys:
+
+ | key | type | default | description |
+ |-------------------|------|---------|-------------------------------------------------------------------------------------------------------------------|
+ | **content_title** | str | *None* | Header of `TimelineContent`. |
+ | **content** | str | *None* | Body of `TimelineContent`. |
+ | **opposite_title**| str | *None* | Header of `TimelineOppositeContent`. |
+ | **opposite** | str | *None* | Body of `TimelineOppositeContent`. |
+ | **color** | str | primary | Color prop of `TimelineDot`. |
+ | **variant** | str | filled | Variant prop of `TimelineDot` (filled/ outlined). |
+ | **icon** | str | *None* | Lowercase name of `Icon`. |
+ | **disable_dot** | bool | *None* | If `True`, the `Icon` is shown standalone and not inside the `TimelineDot`. |
+ """)
+
+ position = param.Selector(default="right", objects=[
+ 'alternate-reverse',
+ 'alternate',
+ 'left',
+ 'right',
+ ], doc="""The position of the content/ opposite content.""")
+
+ _esm_base = "Timeline.jsx"
diff --git a/examples/apps/dashboard/shared/config.py b/examples/apps/dashboard/shared/config.py
new file mode 100644
index 00000000..a0de05dc
--- /dev/null
+++ b/examples/apps/dashboard/shared/config.py
@@ -0,0 +1,55 @@
+import panel as pn
+import param
+from panel_material_ui.base import COLORS as _BUTTON_COLORS, COLOR_ALIASES
+
+BODY_BACKGROUND = "#f5f5f5"
+ICON_CSS = """
+.material-symbols-outlined {
+ font-size: 12px;
+}
+"""
+PAPER_STYLES = {
+ "background": "white",
+ "border-radius": "5px",
+ "border": "1px solid #e5e5e5",
+ "box-shadow": "0 1px 2px 0 rgba(0,0,0,.05)",
+}
+PAGES = [
+ {"label": "Dashboard", "href": "dashboard", "icon": "dashboard"},
+ {"label": "Tables", "href": "tables", "icon": "table_view"},
+ {"label": "Notifications", "href": "notifications", "icon": "notifications"},
+]
+
+pn.config.css_files=[
+ # "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0,0&icon_names=schedule,weekend,person,leaderboard"
+]
+
+_SIDEBAR_BACKGROUND = {"Dark": "#42424a", "Transparent": "inherit", "White": "white"}
+_SIDEBAR_COLOR = {"Dark": "white", "Transparent": "inherit", "White": "black"}
+_BUTTON_COLORS = [color for color in _BUTTON_COLORS if color not in COLOR_ALIASES]
+
+class PageSettings(param.Parameterized):
+ dark_theme = param.Boolean(default=False)
+
+ sidebar_button_color = param.Selector(default="dark", objects=_BUTTON_COLORS)
+ sidebar_background_color = param.Selector(default="White", objects=["Dark", "Transparent", "White"])
+ navbarimportfixed = param.Boolean(default=False)
+
+ def __init__(self, **params):
+ super().__init__(**params)
+
+ pn.state.location.sync(self, parameters=["dark_theme", "sidebar_button_color", "sidebar_background_color"])
+
+ @param.depends("dark_theme", watch=True)
+ def _handle_dark_theme_change(self):
+ pn.config.theme=("dark" if self.dark_theme else "default")
+
+ @param.depends("sidebar_background_color")
+ def sidebar_styles(self):
+ return {"background": _SIDEBAR_BACKGROUND[self.sidebar_background_color], "color": "_SIDEBAR_COLOR", "border-radius": "5px", "margin": "10px"}
+
+ @param.depends("dark_theme")
+ def body_styles(self):
+ if not self.dark_theme:
+ return {"background": BODY_BACKGROUND, "width": "100vw"} # Hack
+ return {"background": "transparent", "width": "100vw"}
diff --git a/examples/apps/dashboard/shared/data.py b/examples/apps/dashboard/shared/data.py
new file mode 100644
index 00000000..ed194157
--- /dev/null
+++ b/examples/apps/dashboard/shared/data.py
@@ -0,0 +1,161 @@
+import pandas as pd
+
+_AUTHORS_DATA = [
+ {
+ "image": "https://randomuser.me/api/portraits/men/1.jpg",
+ "name": "John Michael",
+ "email": "john.michael@example.com",
+ "title": "Manager",
+ "team": "Organization",
+ "status": "Online",
+ "employed": "23/04/18",
+ },
+ {
+ "image": "https://randomuser.me/api/portraits/women/2.jpg",
+ "name": "Alexa Liras",
+ "email": "alexa.liras@example.com",
+ "title": "Programmer",
+ "team": "Developer",
+ "status": "Offline",
+ "employed": "11/01/19",
+ },
+ {
+ "image": "https://randomuser.me/api/portraits/men/3.jpg",
+ "name": "Laurent Perrier",
+ "email": "laurent.perrier@example.com",
+ "title": "Executive",
+ "team": "Projects",
+ "status": "Online",
+ "employed": "19/09/17",
+ },
+ {
+ "image": "https://randomuser.me/api/portraits/women/4.jpg",
+ "name": "Michael Levi",
+ "email": "michael.levi@example.com",
+ "title": "Designer",
+ "team": "Creative",
+ "status": "Online",
+ "employed": "24/12/08",
+ },
+ ]
+
+_PROJECTS_DATA = [
+ {
+ "Company": "Material XD Version",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-xd.svg",
+ "Members": [
+ {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"},
+ {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"},
+ {"Name": "Alexander Smith", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-3.jpg"},
+ {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ ],
+ "Budget": "$14,000",
+ "Completion": 60,
+ },
+ {
+ "Company": "Add Progress Track",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-atlassian.svg",
+ "Members": [
+ {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"},
+ {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ ],
+ "Budget": "$3,000",
+ "Completion": 10,
+ },
+ {
+ "Company": "Fix Platform Errors",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-slack.svg",
+ "Members": [
+ {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"},
+ ],
+ "Budget": "Not set",
+ "Completion": 100,
+ },
+ {
+ "Company": "Launch our Mobile App",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-spotify.svg",
+ "Members": [
+ {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-2.jpg"},
+ {"Name": "Romina Hadid", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-3.jpg"},
+ {"Name": "Alexander Smith", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"},
+ ],
+ "Budget": "$20,500",
+ "Completion": 100,
+ },
+ {
+ "Company": "Add the New Pricing Page",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-jira.svg",
+ "Members": [
+ {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ ],
+ "Budget": "$500",
+ "Completion": 25,
+ },
+ {
+ "Company": "Redesign New Online Shop",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-invision.svg",
+ "Members": [
+ {"Name": "Ryan Tompson", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-1.jpg"},
+ {"Name": "Jessica Doe", "Image": "https://demos.creative-tim.com/material-dashboard/assets/img/team-4.jpg"},
+ ],
+ "Budget": "$2,000",
+ "Completion": 40,
+ }
+]
+
+_PROJECTS_DATA_2 = [
+ {
+ "Company": "Asana",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-asana.svg",
+ "Budget": "$2,500",
+ "Status": "working",
+ "Completion": 60,
+ },
+ {
+ "Company": "Add Progress Track",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-atlassian.svg",
+ "Budget": "$5,000",
+ "Status": "done",
+ "Completion": 10,
+ },
+ {
+ "Company": "Fix Platform Errors",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-slack.svg",
+ "Budget": "$3,400",
+ "Status": "canceled",
+ "Completion": 100,
+ },
+ {
+ "Company": "Launch our Mobile App",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-spotify.svg",
+ "Budget": "$14,000",
+ "Status": "working",
+ "Completion": 100,
+ },
+ {
+ "Company": "Add the New Pricing Page",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-jira.svg",
+ "Budget": "$1,000",
+ "Status": "canceled",
+ "Completion": 25,
+ },
+ {
+ "Company": "Redesign New Online Shop",
+ "CompanyImage": "https://demos.creative-tim.com/material-dashboard/assets/img/small-logos/logo-invision.svg",
+ "Budget": "$2,300",
+ "Status": "done",
+ "Completion": 40,
+ }
+]
+
+
+def get_authors_data():
+ return pd.DataFrame(_AUTHORS_DATA)
+
+def get_project_data():
+ return pd.DataFrame(_PROJECTS_DATA)
+
+def get_project_data_2():
+ return pd.DataFrame(_PROJECTS_DATA_2)
diff --git a/examples/apps/dashboard/shared/page.py b/examples/apps/dashboard/shared/page.py
new file mode 100644
index 00000000..067a8edb
--- /dev/null
+++ b/examples/apps/dashboard/shared/page.py
@@ -0,0 +1,183 @@
+import panel as pn
+import panel_material_ui as pmu
+from shared.components import create_menu
+from shared.config import PageSettings
+
+
+def create_sidebar(name: str, button_color, settings: PageSettings):
+ documentation_link = pmu.widgets.Button(
+ name="Documentation",
+ variant="outlined",
+ href="https://holoviz-dev.github.io/panel-material-ui/",
+ sizing_mode="stretch_width",
+ )
+ reference_link = pmu.widgets.Button(
+ name="Reference",
+ variant="contained",
+ href="https://demos.creative-tim.com/material-dashboard/pages/dashboard",
+ color="dark",
+ sizing_mode="stretch_width",
+ )
+
+ return pn.Column(
+ "####
Panel Material UI",
+ pmu.layout.Divider(sizing_mode="stretch_width", margin=(10, 5, 10, 5)),
+ create_menu(name, button_color=button_color),
+ pn.Spacer(sizing_mode="stretch_height"),
+ documentation_link,
+ reference_link,
+ styles=settings.sidebar_styles,
+ width=300,
+ sizing_mode="stretch_height",
+ )
+
+
+def create_github_star_count():
+ return pn.panel(
+ """\
+
+
+ \
+ """,
+ align="center",
+ )
+
+def create_context(settings: PageSettings):
+ def click(event):
+ button = event.obj
+ settings.sidebar_button_color=button.name
+
+ colors = list(settings.param.sidebar_button_color.objects)[1:7]
+ buttons = [pmu.IconButton(color=color, name=color, icon='circle', description='Select color', margin=0, on_click=click) for color in colors]
+
+ theme_toggle = pmu.ThemeToggle(value=settings.dark_theme, variant="switch")
+ theme_toggle.rx.watch(lambda v: settings.param.update(dark_theme=v))
+
+
+ return pmu.Drawer(
+ pn.Column(
+ "## Material UI Configurator",
+ pn.Row(*buttons, align="center"),
+ """### Sidenav Type
+
+Choose between different sidenav types.""",
+ pmu.widgets.RadioButtonGroup.from_param(settings.param.sidebar_background_color, align="center"),
+ pmu.layout.Divider(sizing_mode="stretch_width", margin=5),
+ theme_toggle,
+ pmu.layout.Divider(sizing_mode="stretch_width", margin=5),
+ pmu.widgets.Button(name="Free Download", href="https://github.com/panel-extensions/panel-material-ui", color="primary", variant="contained", sizing_mode="stretch_width"),
+ pmu.widgets.Button(name="Documentation", href="https://holoviz-dev.github.io/panel-material-ui/", variant="outlined", sizing_mode="stretch_width"),
+ create_github_star_count(),
+ pn.pane.Markdown("## Thank you for sharing!", align="center"),
+ pn.Row(
+ pmu.widgets.Button(name="Tweet", href="https://twitter.com/intent/tweet?text=Check%20Panel%20Material%20UI%20Dashboard%20made%20by%20%40HoloViz%20%23webdesign%20%23dashboard%20%23dataviz&url=https%3A%2F%2Fholoviz-dev.github.io%2Fpanel-material-ui%2F", color="dark", variant="contained"),
+ pmu.widgets.Button(name="Share", href="https://www.facebook.com/sharer/sharer.php?u=https://holoviz-dev.github.io/panel-material-ui", color="dark", variant="contained"),
+ align="center"
+ ),
+ sizing_mode="stretch_width",
+ ),
+ anchor="right", size=400
+ )
+
+
+
+def create_header(name: str, settings_callback):
+ items = [
+ {'label': 'Home', "href": "./"},
+ {'label': f'{name}'},
+ ]
+ bread_crumbs = pmu.menus.Breadcrumbs(
+ name="Breadcrumbs test", items=items, align="center"
+ )
+ search = pmu.widgets.TextInput(
+ name="Type here...", placeholder="Search", size="small"
+ )
+ goto_online_pmu = pmu.widgets.Button(
+ name="Panel Material UI",
+ variant="outlined",
+ color="info",
+ href="https://panel.holoviz.org",
+ align="center",
+ )
+ github_star_count = create_github_star_count()
+ settings_button = pmu.widgets.ButtonIcon(
+ name="Context", icon="settings", align="center", on_click=settings_callback
+ )
+
+ return pn.Row(
+ bread_crumbs,
+ pn.Spacer(sizing_mode="stretch_width"),
+ search,
+ goto_online_pmu,
+ github_star_count,
+ settings_button,
+ sizing_mode="stretch_width",
+ )
+
+
+def create_footer():
+ return pn.Row(
+ "© 2025, made with by **panel-material-ui** for beautiful data apps.",
+ pn.Spacer(sizing_mode="stretch_width"),
+ pmu.widgets.Button(
+ name="Panel Material UI",
+ variant="text",
+ href="https://panel-extensions.github.io/panel-material-ui/",
+ ),
+ pmu.widgets.Button(
+ name="About Us",
+ variant="text",
+ href="https://panel.holoviz.org",
+ ),
+ pmu.widgets.Button(
+ name="Blog",
+ variant="text",
+ href="https://blog.holoviz.org/",
+ ),
+ pmu.widgets.Button(
+ name="License",
+ variant="text",
+ href="https://panel-extensions.github.io/panel-material-ui/LICENSE.md",
+ )
+ )
+
+
+def create_fab(settings_callback):
+ sx ={
+ "position": 'fixed',
+ "bottom": "64px",
+ "right": 24,
+ }
+ fab_button = pmu.Fab(color='default', label='Click me', icon="settings", size="large", description="Toggle settings drawer", margin=0, on_click=settings_callback, sx=sx)
+ return fab_button
+
+
+def create_page(name: str, main: list):
+ settings = PageSettings()
+
+ sidebar = create_sidebar(name=name, button_color=settings.param.sidebar_button_color, settings=settings)
+
+ context = create_context(settings=settings)
+
+ def toggle_drawer(event):
+ context.open = not context.open
+
+ header = create_header(name=name, settings_callback=toggle_drawer)
+
+ footer = create_footer()
+ fab_row = create_fab(settings_callback=toggle_drawer)
+ main = pn.Column(
+ header,
+ *main,
+ pn.layout.VSpacer(),
+ fab_row,
+ footer,
+ sizing_mode="stretch_width",
+ margin=10,
+ )
+
+ return pn.Column(
+ pn.Row(sidebar, main, context),
+ sizing_mode="stretch_both",
+ styles=settings.body_styles,
+ )
diff --git a/examples/apps/dashboard/shared/plots.py b/examples/apps/dashboard/shared/plots.py
new file mode 100644
index 00000000..035517da
--- /dev/null
+++ b/examples/apps/dashboard/shared/plots.py
@@ -0,0 +1,70 @@
+_WEBSITE_VIEWS_DATA = {
+ "xAxis": {"data": ["M", "T", "W", "T", "F", "S", "S"]},
+ "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
+ "yAxis": {},
+ "series": [
+ {
+ "name": "Views",
+ "type": "bar",
+ "data": [50, 45, 30, 35, 50, 60, 75],
+ "itemStyle": {"color": "green"},
+ }
+ ],
+ "grid": {
+ "top": 10,
+ "bottom": 20,
+ },
+}
+
+_DAILY_SALES = {
+ "xAxis": {
+ "type": "category",
+ "data": ["J", "F", "M", "A", "M", "J", "J", "A", "S", "O", "N", "D"],
+ },
+ "yAxis": {"type": "value"},
+ "series": [
+ {
+ "name": "Monthly Data",
+ "type": "line",
+ "data": [120, 132, 101, 134, 90, 230, 210, 180, 150, 200, 170, 250],
+ "itemStyle": {"color": "green"},
+ }
+ ],
+ "grid": {
+ "top": 10,
+ "bottom": 20,
+ },
+ "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
+}
+_COMPLETED_TASKS = {
+ "xAxis": {
+ "type": "category",
+ "data": ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
+ },
+ "yAxis": {"type": "value"},
+ "series": [
+ {
+ "name": "Monthly Data",
+ "type": "line",
+ "data": [50, 50, 300, 210, 500, 230, 400, 230, 525],
+ "itemStyle": {"color": "green"},
+ }
+ ],
+ "grid": {
+ "top": 10,
+ "bottom": 20,
+ },
+ "tooltip": {"trigger": "axis", "axisPointer": {"type": "shadow"}},
+}
+
+
+def get_website_views_config():
+ return _WEBSITE_VIEWS_DATA
+
+
+def get_daily_sales_config():
+ return _DAILY_SALES
+
+
+def get_completed_tasks_config():
+ return _COMPLETED_TASKS
diff --git a/examples/apps/dashboard/shared/tables.py b/examples/apps/dashboard/shared/tables.py
new file mode 100644
index 00000000..f35ea8fa
--- /dev/null
+++ b/examples/apps/dashboard/shared/tables.py
@@ -0,0 +1,71 @@
+import pandas as pd
+
+def generate_company_html(row: pd.Series, image_width: str='40px'):
+ company_name=row["Company"]
+ company_image_url=row["CompanyImage"]
+ html = f'''
+
+

+
{company_name}
+
+ '''
+ return html
+
+def generate_progress_bar(row: pd.Series):
+ completion_percent = int(round(float(row["Completion"]),0))
+ color = 'green' if completion_percent >=99.999 else 'blue'
+ html = f'''
+
+
{completion_percent}%
+
+
+ '''
+ return html
+
+def generate_member_images(row: pd.Series, image_size: str='30px', overlap_offset: str='-10px'):
+ members = row["Members"]
+ images_html = ''
+ for idx, member in enumerate(members):
+ images_html += f'''
+
+ '''
+ wrapper_html = f'''
+
+ {images_html}
+
+ '''
+ return wrapper_html
+
+def render_author(row: pd.Series):
+ return (
+ f''
+ f'

'
+ f'
{row["name"]}
{row["email"]}
'
+ f"
"
+ )
+
+def render_function(row: pd.Series):
+ return f'{row["title"]}
{row["team"]}
'
+
+def render_status(row: pd.Series):
+ color = "#43a047" if row["status"] == "Online" else "#747b8a"
+
+ return f'{row["status"]}'
+
+def render_edit_button():
+ return ''
diff --git a/examples/apps/dashboard/tables.py b/examples/apps/dashboard/tables.py
new file mode 100644
index 00000000..79a90f28
--- /dev/null
+++ b/examples/apps/dashboard/tables.py
@@ -0,0 +1,130 @@
+import panel as pn
+import panel_material_ui as pmu
+import pandas as pd
+
+from shared.data import get_authors_data, get_project_data_2
+from shared.page import create_page
+from shared.components import create_card_with_jumbo_header
+from shared import tables as table_func
+
+pn.extension()
+
+
+@pn.cache
+def to_styled_authors_table(data):
+ data["Author"] = data.apply(table_func.render_author, axis=1)
+ data["Function"] = data.apply(table_func.render_function, axis=1)
+ data["Status"] = data.apply(table_func.render_status, axis=1)
+ data["Edit"] = table_func.render_edit_button()
+ data = data.rename(columns={"employed": "Employed"})
+ data = data[
+ [
+ "Author",
+ "Function",
+ "Status",
+ "Employed",
+ "Edit",
+ ]
+ ]
+
+ styled_df = data.style
+ styled_df = (
+ styled_df.hide(axis="index")
+ .set_table_styles(
+ [
+ {
+ "selector": "th",
+ "props": [
+ ("background-color", "white"),
+ ("font-weight", "bold"),
+ ("border-bottom", "1px solid black"),
+ ("text-align", "left"),
+ ],
+ },
+ {
+ "selector": "td",
+ "props": [
+ ("padding", "10px"),
+ ("background", "white"),
+ ("border-top", "1px solid black"),
+ ],
+ },
+ {
+ "selector": ".col0, .col1, .col2, .col3, .col4",
+ "props": [
+ ("width", "20%"),
+ ],
+ },
+ ]
+ )
+ .set_properties(
+ **{"text-align": "left"},
+ )
+ )
+
+ return styled_df
+
+
+
+@pn.cache
+def to_styled_projects_2_table(data: pd.DataFrame):
+
+ data["Company"] = data.apply(table_func.generate_company_html, axis=1)
+ data["Completion"] = data.apply(table_func.generate_progress_bar, axis=1)
+ data = data.drop(columns=["CompanyImage"])
+ styled_df = data.style
+ styled_df = (
+ styled_df.hide(axis="index")
+ .set_table_styles(
+ [
+ {
+ "selector": "th",
+ "props": [
+ ("background-color", "white"),
+ ("font-weight", "bold"),
+ ("border-bottom", "1px solid black"),
+ ("text-align", "left"),
+ ],
+ },
+ {
+ "selector": "td",
+ "props": [
+ ("padding", "10px"),
+ ("background", "white"),
+ ("border-top", "1px solid black"),
+ ],
+ },
+ {
+ "selector": ".col0, .col1, .col2, .col3",
+ "props": [
+ ("width", "20%"),
+ ],
+ },
+ ]
+ )
+ .set_properties(
+ **{"text-align": "left"},
+ )
+ )
+
+ return styled_df
+
+
+authors_data = get_authors_data()
+styled_authors_table = to_styled_authors_table(authors_data)
+authors_table = pn.panel(styled_authors_table, sizing_mode="stretch_width")
+authors_card = create_card_with_jumbo_header("### Authors Table", authors_table)
+
+projects_data_2 = get_project_data_2()
+styled_projects_2_table = to_styled_projects_2_table(projects_data_2)
+projects_2_table = pn.panel(styled_projects_2_table, sizing_mode="stretch_width")
+projects_2_card = create_card_with_jumbo_header("### Project Table", projects_2_table)
+
+main = pn.Column(
+ pn.Spacer(height=25),
+ authors_card,
+ projects_2_card,
+ pn.Spacer(sizing_mode="stretch_both"),
+ sizing_mode="stretch_both",
+)
+create_page(name="Tables", main=[main]).servable(title="Tables")
diff --git a/examples/reference/layouts/Drawer.ipynb b/examples/reference/layouts/Drawer.ipynb
index 70b1d9ba..e379eebd 100644
--- a/examples/reference/layouts/Drawer.ipynb
+++ b/examples/reference/layouts/Drawer.ipynb
@@ -18,9 +18,12 @@
"id": "a6c6a705-0ea1-4a19-a264-eacac2b2544e",
"metadata": {},
"source": [
- "The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality.\n",
+ "The `Drawer` component (akin to a sidebar) provides ergonomic access to destinations in a site or app functionality such as switching accounts.\n",
+ "\n",
+ "The `Drawer` is built on top of the [Material UI `Drawer`](https://mui.com/material-ui/react-drawer/) and mimics its API.\n",
"\n",
"## Parameters:\n",
+ "\n",
"For details on other options for customizing the component see the layout and styling how-to guides.\n",
"\n",
"### Core\n",
@@ -165,7 +168,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.12.2"
+ "version": "3.12.9"
}
},
"nbformat": 4,
diff --git a/examples/reference/widgets/Button.ipynb b/examples/reference/widgets/Button.ipynb
index b6c3a39b..c3753105 100644
--- a/examples/reference/widgets/Button.ipynb
+++ b/examples/reference/widgets/Button.ipynb
@@ -52,6 +52,7 @@
"* **`icon_size`** (str): Size of the icon as a string, e.g. 12px or 1em.\n",
"* **`label`** (str): The title of the widget.\n",
"* **`variant`** (str): The button style, either 'solid', 'outlined', 'text'.\n",
+ "* **`href`** (str): A url. Turns the button into a link.\n",
"\n",
"##### Styling\n",
"\n",
diff --git a/src/panel_material_ui/layout/base.py b/src/panel_material_ui/layout/base.py
index 9c6ebf75..3cc97729 100644
--- a/src/panel_material_ui/layout/base.py
+++ b/src/panel_material_ui/layout/base.py
@@ -562,7 +562,6 @@ class Dialog(MaterialListLike):
_esm_base = "Dialog.jsx"
-
class Drawer(MaterialListLike):
"""
The `Drawer` component can be used to display important content in a modal-like overlay that requires
@@ -623,7 +622,6 @@ def create_toggle(
toggle.jslink(self, value='open', bidirectional=True)
return toggle
-
__all__ = [
"Accordion",
"Alert",
diff --git a/src/panel_material_ui/widgets/ChangeIndicator.jsx b/src/panel_material_ui/widgets/ChangeIndicator.jsx
new file mode 100644
index 00000000..cece6950
--- /dev/null
+++ b/src/panel_material_ui/widgets/ChangeIndicator.jsx
@@ -0,0 +1,74 @@
+import {Card, CardContent, Typography, Box, Icon, Divider} from "@mui/material";
+
+/**
+ * A reusable change indicator card displaying a metric, its change, and an icon.
+ *
+ * Props:
+ * - title: Label for the metric
+ * - icon: Name of the Material Icon to display
+ * - value: Current value of the metric
+ * - changePercent: Percentage change (positive or negative)
+ * - since: Text describing the period, e.g. "Since last week"
+ */
+const ChangeIndicator = ({title, icon, value, changePercent, since}) => {
+ const isPositive = changePercent > 0;
+ const changeColor = isPositive ? "success.main" : "error.main";
+ const changeSymbol = isPositive ? "+" : "";
+
+ return (
+
+
+ {icon}
+
+
+
+
+ {title}
+
+
+
+ {value}
+
+
+
+
+ {`${changeSymbol}${changePercent}%`}
+
+ {since}
+
+
+
+ );
+};
+
+export function render({model}) {
+ const [title]=model.useState("title")
+ const [icon]=model.useState("icon")
+ const [value]=model.useState("value")
+ const [change_percent]=model.useState("change_percent")
+ const [since]=model.useState("since")
+ return
+}
diff --git a/src/panel_material_ui/widgets/Icon.jsx b/src/panel_material_ui/widgets/Icon.jsx
new file mode 100644
index 00000000..c861ae0a
--- /dev/null
+++ b/src/panel_material_ui/widgets/Icon.jsx
@@ -0,0 +1,16 @@
+import Icon from "@mui/material/Icon";
+
+export function render({model}) {
+ const [icon] = model.useState("value")
+ const [color] = model.useState("color")
+ const [fontSize] = model.useState("font_size")
+ const [sx] = model.useState("sx")
+ return (
+ {icon}
+
+ )
+}
diff --git a/src/panel_material_ui/widgets/List.jsx b/src/panel_material_ui/widgets/List.jsx
index 2c590a71..de7640ef 100644
--- a/src/panel_material_ui/widgets/List.jsx
+++ b/src/panel_material_ui/widgets/List.jsx
@@ -102,6 +102,7 @@ export function render({model}) {
model.send_msg({type: "click", item: path})
}}
selected={highlight && isActive}
+ href={href}
sx={{
p: `0 4px 0 ${(indent+1) * level_indent}px`,
"&.MuiListItemButton-root.Mui-selected": {
diff --git a/src/panel_material_ui/widgets/indicators.py b/src/panel_material_ui/widgets/indicators.py
index 26adf31c..77a3141b 100644
--- a/src/panel_material_ui/widgets/indicators.py
+++ b/src/panel_material_ui/widgets/indicators.py
@@ -85,8 +85,43 @@ def _update_value(self, *_, **__):
if self.value == -1 and self.variant == "determinate":
self.variant = "indeterminate"
+# Generalize or move to dashboard example
+# Could not get icon from ReactComponent so included it here
+
+class ChangeIndicator(MaterialWidget):
+ value = param.String(default="")
+ title = param.String()
+ icon = param.String()
+ change_percent = param.Number()
+ since = param.String()
+
+ _esm_base = "ChangeIndicator.jsx"
+
+ _importmap = {
+ "imports": {
+ "@mui/material": "https://esm.sh/@mui/material@6.4.9",
+ }
+ }
+
+class Icon(MaterialWidget):
+ """The Icon component will display an icon from the Material UI Icon set.
+
+ References:
+
+ - https://mui.com/material-ui/icons/#icon-font-icons
+ - https://mui.com/material-ui/api/icon/
+ """
+ value = param.String(default="", doc="The snake cased name of the icon font ligature.")
+ color = param.String(default="inherit", doc="""The color of the component.
+ It supports both default and custom theme colors""")
+ font_size= param.String(default="medium", doc="""The fontSize applied to the icon.
+ Defaults to medium, but can be configured to inherit font size.""")
+
+ _esm_base="Icon.jsx"
__all__ = [
+ "ChangeIndicator",
+ "Icon",
"LoadingSpinner",
- "Progress"
+ "Progress",
]