From 6c655ba36c74f0fd844e828dcd2295af38fd3077 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:16:24 +0000
Subject: [PATCH 1/8] Initial plan
From 09366760a108f6d92eb0d30b3fff0bca5f501dd2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 29 Sep 2025 11:27:17 +0000
Subject: [PATCH 2/8] Implement draggable sidebar functionality with drag
handle and state management
Co-authored-by: ahuang11 <15331990+ahuang11@users.noreply.github.com>
---
src/panel_material_ui/template/Page.jsx | 104 +++++++++++++++++++++++-
1 file changed, 100 insertions(+), 4 deletions(-)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index 506ca04a..f21a5dce 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -56,8 +56,16 @@ export function render({model, view}) {
const [dark_theme, setDarkTheme] = model.useState("dark_theme")
const [logo] = model.useState("logo")
const [open, setOpen] = model.useState("sidebar_open")
- const [sidebar_width] = model.useState("sidebar_width")
+ const [sidebar_width, setSidebarWidth] = model.useState("sidebar_width")
const [theme_toggle] = model.useState("theme_toggle")
+
+ // Draggable sidebar state and constants
+ const [isDragging, setIsDragging] = React.useState(false)
+ const [dragStartX, setDragStartX] = React.useState(0)
+ const [dragStartWidth, setDragStartWidth] = React.useState(0)
+ const MIN_SIDEBAR_WIDTH = 180
+ const MAX_SIDEBAR_WIDTH = 600
+ const DEFAULT_SIDEBAR_WIDTH = 240
const [site_url] = model.useState("site_url")
const [title] = model.useState("title")
const [variant] = model.useState("sidebar_variant")
@@ -136,7 +144,58 @@ export function render({model, view}) {
setup_global_styles(view, theme)
React.useEffect(() => dark_mode.set_value(dark_theme), [dark_theme])
+ // Drag handlers for sidebar resizing
+ const handleDragStart = React.useCallback((e) => {
+ const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
+ setIsDragging(true)
+ setDragStartX(clientX)
+ setDragStartWidth(sidebar_width || DEFAULT_SIDEBAR_WIDTH)
+ e.preventDefault()
+ }, [sidebar_width])
+
+ const handleDragMove = React.useCallback((e) => {
+ if (!isDragging) return
+
+ const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
+ const deltaX = clientX - dragStartX
+ const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, dragStartWidth + deltaX))
+
+ // Update width immediately for responsive feedback
+ setSidebarWidth(newWidth)
+ e.preventDefault()
+ }, [isDragging, dragStartX, dragStartWidth])
+
+ const handleDragEnd = React.useCallback(() => {
+ setIsDragging(false)
+ }, [])
+
+ // Add global mouse/touch event listeners when dragging
+ React.useEffect(() => {
+ if (isDragging) {
+ const handleMouseMove = (e) => handleDragMove(e)
+ const handleMouseUp = () => handleDragEnd()
+ const handleTouchMove = (e) => handleDragMove(e)
+ const handleTouchEnd = () => handleDragEnd()
+
+ document.addEventListener('mousemove', handleMouseMove)
+ document.addEventListener('mouseup', handleMouseUp)
+ document.addEventListener('touchmove', handleTouchMove, { passive: false })
+ document.addEventListener('touchend', handleTouchEnd)
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove)
+ document.removeEventListener('mouseup', handleMouseUp)
+ document.removeEventListener('touchmove', handleTouchMove)
+ document.removeEventListener('touchend', handleTouchEnd)
+ }
+ }
+ }, [isDragging, handleDragMove, handleDragEnd])
+
const drawer_variant = variant === "auto" ? (isMobile ? "temporary": "persistent") : variant
+
+ // Use fallback width if sidebar_width is not set
+ const effectiveSidebarWidth = sidebar_width || DEFAULT_SIDEBAR_WIDTH
+
const drawer = sidebar.length > 0 ? (
@@ -161,6 +224,39 @@ export function render({model, view}) {
return object
})}
+ {/* Drag handle for resizing sidebar */}
+
) : null
@@ -270,13 +366,13 @@ export function render({model, view}) {
drawer_variant === "temporary" ? (
{width: 0, flexShrink: {xs: 0}}
) : (
- {width: {sm: sidebar_width}, flexShrink: {sm: 0}}
+ {width: {sm: effectiveSidebarWidth}, flexShrink: {sm: 0}}
)
}
>
{drawer}
}
-
+
From fdfe14e74da1ad68e39e291091b9f15930aa9a83 Mon Sep 17 00:00:00 2001
From: Andrew Huang
Date: Mon, 29 Sep 2025 16:11:12 -0700
Subject: [PATCH 3/8] add param, tests, and better defaults
---
src/panel_material_ui/template/Page.jsx | 96 +++++++-------
src/panel_material_ui/template/base.py | 2 +
tests/ui/template/test_page.py | 164 +++++++++++++++++++++++-
3 files changed, 212 insertions(+), 50 deletions(-)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index f21a5dce..1f0575d3 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -56,6 +56,7 @@ export function render({model, view}) {
const [dark_theme, setDarkTheme] = model.useState("dark_theme")
const [logo] = model.useState("logo")
const [open, setOpen] = model.useState("sidebar_open")
+ const [sidebar_resizable] = model.useState("sidebar_resizable")
const [sidebar_width, setSidebarWidth] = model.useState("sidebar_width")
const [theme_toggle] = model.useState("theme_toggle")
@@ -63,9 +64,6 @@ export function render({model, view}) {
const [isDragging, setIsDragging] = React.useState(false)
const [dragStartX, setDragStartX] = React.useState(0)
const [dragStartWidth, setDragStartWidth] = React.useState(0)
- const MIN_SIDEBAR_WIDTH = 180
- const MAX_SIDEBAR_WIDTH = 600
- const DEFAULT_SIDEBAR_WIDTH = 240
const [site_url] = model.useState("site_url")
const [title] = model.useState("title")
const [variant] = model.useState("sidebar_variant")
@@ -149,21 +147,26 @@ export function render({model, view}) {
const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
setIsDragging(true)
setDragStartX(clientX)
- setDragStartWidth(sidebar_width || DEFAULT_SIDEBAR_WIDTH)
+ setDragStartWidth(sidebar_width)
e.preventDefault()
}, [sidebar_width])
const handleDragMove = React.useCallback((e) => {
if (!isDragging) return
-
+
const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
const deltaX = clientX - dragStartX
- const newWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, dragStartWidth + deltaX))
-
- // Update width immediately for responsive feedback
- setSidebarWidth(newWidth)
+ const newWidth = dragStartWidth + deltaX
+
+ // If width gets close to 0, collapse the sidebar completely
+ if (newWidth < 50) {
+ setOpen(false)
+ } else {
+ // Update width immediately for responsive feedback
+ setSidebarWidth(newWidth)
+ }
e.preventDefault()
- }, [isDragging, dragStartX, dragStartWidth])
+ }, [isDragging, dragStartX, dragStartWidth, setOpen])
const handleDragEnd = React.useCallback(() => {
setIsDragging(false)
@@ -192,10 +195,7 @@ export function render({model, view}) {
}, [isDragging, handleDragMove, handleDragEnd])
const drawer_variant = variant === "auto" ? (isMobile ? "temporary": "persistent") : variant
-
- // Use fallback width if sidebar_width is not set
- const effectiveSidebarWidth = sidebar_width || DEFAULT_SIDEBAR_WIDTH
-
+
const drawer = sidebar.length > 0 ? (
- {/* Drag handle for resizing sidebar */}
-
+ cursor: "col-resize",
+ backgroundColor: "transparent",
+ borderRight: `1px solid ${theme.palette.divider}`,
+ zIndex: 1000,
+ "&:hover": {
+ backgroundColor: theme.palette.action.hover,
+ borderRightWidth: "2px",
+ borderRightColor: theme.palette.primary.main,
+ },
+ // Make the handle slightly larger for easier interaction
+ "&:before": {
+ content: '""',
+ position: "absolute",
+ top: 0,
+ right: "-3px", // Extend hit area beyond visual boundary
+ width: "6px",
+ height: "100%",
+ backgroundColor: "transparent"
+ }
+ }}
+ aria-label="Resize sidebar"
+ title="Drag to resize sidebar"
+ />
+ )}
) : null
@@ -366,13 +368,13 @@ export function render({model, view}) {
drawer_variant === "temporary" ? (
{width: 0, flexShrink: {xs: 0}}
) : (
- {width: {sm: effectiveSidebarWidth}, flexShrink: {sm: 0}}
+ {width: {sm: sidebar_width}, flexShrink: {sm: 0}}
)
}
>
{drawer}
}
-
+
diff --git a/src/panel_material_ui/template/base.py b/src/panel_material_ui/template/base.py
index c08d164f..d555e17c 100644
--- a/src/panel_material_ui/template/base.py
+++ b/src/panel_material_ui/template/base.py
@@ -103,6 +103,8 @@ class Page(MaterialComponent, ResourceComponent):
sidebar_open = param.Boolean(default=True, doc="Whether the sidebar is open or closed.")
+ sidebar_resizable = param.Boolean(default=True, doc="Whether the sidebar can be resized by dragging.")
+
sidebar_variant = param.Selector(default="auto", objects=SIDEBAR_VARIANTS, doc="""
Whether the sidebar is persistent, a temporary drawer, a permanent drawer, or automatically
switches between the two based on screen size.""")
diff --git a/tests/ui/template/test_page.py b/tests/ui/template/test_page.py
index a3662be4..c32d4c36 100644
--- a/tests/ui/template/test_page.py
+++ b/tests/ui/template/test_page.py
@@ -2,10 +2,11 @@
pytest.importorskip('playwright')
-from panel_material_ui.template import Page
-
-from playwright.sync_api import expect
+import panel as pn
from panel.tests.util import serve_component
+from playwright.sync_api import expect
+
+from panel_material_ui.template import Page
pytestmark = pytest.mark.ui
@@ -26,3 +27,160 @@ def test_page_theme_config_header_color(page):
}
}
expect(header).to_have_css("background-color", "rgb(0, 0, 0)")
+
+
+def test_page_sidebar_resizable_handle_present(page):
+ """Test that the resize handle is present when sidebar has content."""
+ pg = Page(sidebar=[pn.pane.Markdown("# Sidebar Content")])
+
+ serve_component(page, pg)
+
+ # Check that sidebar is present
+ sidebar = page.locator(".sidebar")
+ expect(sidebar).to_be_visible()
+
+ # Check that resize handle is present
+ resize_handle = page.locator('[aria-label="Resize sidebar"]')
+ expect(resize_handle).to_be_visible()
+ expect(resize_handle).to_have_attribute("title", "Drag to resize sidebar")
+
+
+def test_page_sidebar_default_width(page):
+ """Test that sidebar has the default width of 320px."""
+ pg = Page(sidebar=[pn.pane.Markdown("# Sidebar Content")])
+
+ serve_component(page, pg)
+
+ sidebar_paper = page.locator(".MuiDrawer-paper.sidebar")
+ expect(sidebar_paper).to_have_css("width", "320px")
+
+
+def test_page_sidebar_custom_width(page):
+ """Test that sidebar respects custom width setting."""
+ pg = Page(
+ sidebar=[pn.pane.Markdown("# Sidebar Content")],
+ sidebar_width=400
+ )
+
+ serve_component(page, pg)
+
+ sidebar_paper = page.locator(".MuiDrawer-paper.sidebar")
+ expect(sidebar_paper).to_have_css("width", "400px")
+
+
+def test_page_sidebar_resize_drag(page):
+ """Test that dragging the resize handle changes sidebar width."""
+ pg = Page(sidebar=[pn.pane.Markdown("# Sidebar Content")])
+
+ serve_component(page, pg)
+
+ # Get initial sidebar width
+ sidebar_paper = page.locator(".MuiDrawer-paper.sidebar")
+ expect(sidebar_paper).to_have_css("width", "320px")
+
+ # Get resize handle
+ resize_handle = page.locator('[aria-label="Resize sidebar"]')
+ expect(resize_handle).to_be_visible()
+
+ # Get the bounding box for drag calculation
+ handle_box = resize_handle.bounding_box()
+ assert handle_box is not None
+
+ # Drag the handle to the right to increase width
+ page.mouse.move(handle_box["x"] + handle_box["width"] / 2, handle_box["y"] + handle_box["height"] / 2)
+ page.mouse.down()
+ page.mouse.move(handle_box["x"] + 100, handle_box["y"] + handle_box["height"] / 2)
+ page.mouse.up()
+
+ # Wait for the change to be applied
+ page.wait_for_timeout(100)
+
+ # Check that the width has increased (should be around 420px)
+ # Using a range check since exact pixel values can vary
+ assert pg.sidebar_width > 380, f"Expected sidebar_width > 380, got {pg.sidebar_width}"
+ assert pg.sidebar_width < 440, f"Expected sidebar_width < 440, got {pg.sidebar_width}"
+
+
+def test_page_sidebar_collapse_on_small_drag(page):
+ """Test that dragging sidebar to very small width collapses it."""
+ pg = Page(
+ sidebar=[pn.pane.Markdown("# Sidebar Content")],
+ sidebar_width=200 # Start with smaller width for easier testing
+ )
+
+ serve_component(page, pg)
+
+ # Verify sidebar is initially open
+ assert pg.sidebar_open is True
+ sidebar_paper = page.locator(".MuiDrawer-paper.sidebar")
+ expect(sidebar_paper).to_be_visible()
+
+ # Get resize handle
+ resize_handle = page.locator('[aria-label="Resize sidebar"]')
+ expect(resize_handle).to_be_visible()
+
+ # Get the bounding box for drag calculation
+ handle_box = resize_handle.bounding_box()
+ assert handle_box is not None
+
+ # Drag the handle far to the left to trigger collapse (more than 150px to get below 50px threshold)
+ page.mouse.move(handle_box["x"] + handle_box["width"] / 2, handle_box["y"] + handle_box["height"] / 2)
+ page.mouse.down()
+ page.mouse.move(handle_box["x"] - 180, handle_box["y"] + handle_box["height"] / 2)
+ page.mouse.up()
+
+ # Wait for the change to be applied
+ page.wait_for_timeout(200)
+
+ # Check that sidebar is now collapsed
+ assert pg.sidebar_open is False, "Sidebar should be collapsed when dragged to small width"
+
+
+def test_page_sidebar_no_handle_when_empty(page):
+ """Test that no resize handle is present when sidebar is empty."""
+ pg = Page() # No sidebar content
+
+ serve_component(page, pg)
+
+ # Check that resize handle is not present
+ resize_handle = page.locator('[aria-label="Resize sidebar"]')
+ expect(resize_handle).not_to_be_visible()
+
+
+def test_page_sidebar_handle_styling(page):
+ """Test that the resize handle has proper styling and hover effects."""
+ pg = Page(sidebar=[pn.pane.Markdown("# Sidebar Content")])
+
+ serve_component(page, pg)
+
+ resize_handle = page.locator('[aria-label="Resize sidebar"]')
+ expect(resize_handle).to_be_visible()
+
+ # Check that handle has col-resize cursor
+ expect(resize_handle).to_have_css("cursor", "col-resize")
+
+ # Check that handle is positioned at the right edge
+ expect(resize_handle).to_have_css("position", "absolute")
+ expect(resize_handle).to_have_css("right", "0px")
+ expect(resize_handle).to_have_css("top", "0px")
+
+
+def test_page_sidebar_width_persistence(page):
+ """Test that sidebar width changes are reflected in the model."""
+ pg = Page(sidebar=[pn.pane.Markdown("# Sidebar Content")])
+
+ serve_component(page, pg)
+
+ # Get initial width from model
+ initial_width = pg.sidebar_width
+ assert initial_width == 320
+
+ # Simulate a programmatic width change
+ pg.sidebar_width = 450
+
+ # Wait for change to be applied
+ page.wait_for_timeout(100)
+
+ # Check that the CSS reflects the new width
+ sidebar_paper = page.locator(".MuiDrawer-paper.sidebar")
+ expect(sidebar_paper).to_have_css("width", "450px")
From 8461b004797ac23c84eb9bb8f2cd7b37e10c9f0b Mon Sep 17 00:00:00 2001
From: Andrew Huang
Date: Mon, 29 Sep 2025 16:19:21 -0700
Subject: [PATCH 4/8] eslint
---
src/panel_material_ui/template/Page.jsx | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index 1f0575d3..9069ef85 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -144,7 +144,7 @@ export function render({model, view}) {
// Drag handlers for sidebar resizing
const handleDragStart = React.useCallback((e) => {
- const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
+ const clientX = e.type.startsWith("touch") ? e.touches[0].clientX : e.clientX
setIsDragging(true)
setDragStartX(clientX)
setDragStartWidth(sidebar_width)
@@ -152,9 +152,9 @@ export function render({model, view}) {
}, [sidebar_width])
const handleDragMove = React.useCallback((e) => {
- if (!isDragging) return
+ if (!isDragging) { return }
- const clientX = e.type.startsWith('touch') ? e.touches[0].clientX : e.clientX
+ const clientX = e.type.startsWith("touch") ? e.touches[0].clientX : e.clientX
const deltaX = clientX - dragStartX
const newWidth = dragStartWidth + deltaX
@@ -180,16 +180,16 @@ export function render({model, view}) {
const handleTouchMove = (e) => handleDragMove(e)
const handleTouchEnd = () => handleDragEnd()
- document.addEventListener('mousemove', handleMouseMove)
- document.addEventListener('mouseup', handleMouseUp)
- document.addEventListener('touchmove', handleTouchMove, { passive: false })
- document.addEventListener('touchend', handleTouchEnd)
+ document.addEventListener("mousemove", handleMouseMove)
+ document.addEventListener("mouseup", handleMouseUp)
+ document.addEventListener("touchmove", handleTouchMove, {passive: false})
+ document.addEventListener("touchend", handleTouchEnd)
return () => {
- document.removeEventListener('mousemove', handleMouseMove)
- document.removeEventListener('mouseup', handleMouseUp)
- document.removeEventListener('touchmove', handleTouchMove)
- document.removeEventListener('touchend', handleTouchEnd)
+ document.removeEventListener("mousemove", handleMouseMove)
+ document.removeEventListener("mouseup", handleMouseUp)
+ document.removeEventListener("touchmove", handleTouchMove)
+ document.removeEventListener("touchend", handleTouchEnd)
}
}
}, [isDragging, handleDragMove, handleDragEnd])
From 7e57cf2f2d64056ec3f4cf413f319436ea90ea56 Mon Sep 17 00:00:00 2001
From: Andrew Huang
Date: Tue, 30 Sep 2025 04:29:18 -0700
Subject: [PATCH 5/8] height
---
src/panel_material_ui/template/Page.jsx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index 9069ef85..11b3c463 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -207,8 +207,10 @@ export function render({model, view}) {
display: "flex",
flexDirection: "column",
flexShrink: 0,
+ height: "100vh", // Full viewport height
[`& .MuiDrawer-paper`]: {
width: sidebar_width,
+ height: "100vh", // Full viewport height
boxSizing: "border-box",
position: "relative" // Enable positioning for drag handle
},
From a2cb35b13c0a03aaf6aab59e9454aec014e9d01e Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 3 Nov 2025 13:15:58 +0100
Subject: [PATCH 6/8] Small tweaks
---
src/panel_material_ui/template/Page.jsx | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index 11b3c463..47455235 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -200,7 +200,6 @@ export function render({model, view}) {
setOpen(false)) : null}
sx={{
@@ -212,7 +211,8 @@ export function render({model, view}) {
width: sidebar_width,
height: "100vh", // Full viewport height
boxSizing: "border-box",
- position: "relative" // Enable positioning for drag handle
+ position: "relative", // Enable positioning for drag handle
+ overflowX: "hidden"
},
}}
variant={drawer_variant}
@@ -226,7 +226,6 @@ export function render({model, view}) {
return object
})}
- {/* Drag handle for resizing sidebar - only show if resizable */}
{sidebar_resizable && (
contextOpen(false)}
sx={{
From 989f038e22329094dcca05cd0fe55add4a1721eb Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 3 Nov 2025 13:25:31 +0100
Subject: [PATCH 7/8] Small fixes
---
src/panel_material_ui/template/Page.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/panel_material_ui/template/Page.jsx b/src/panel_material_ui/template/Page.jsx
index 47455235..0d505e05 100644
--- a/src/panel_material_ui/template/Page.jsx
+++ b/src/panel_material_ui/template/Page.jsx
@@ -163,7 +163,7 @@ export function render({model, view}) {
setOpen(false)
} else {
// Update width immediately for responsive feedback
- setSidebarWidth(newWidth)
+ setSidebarWidth(Math.round(newWidth))
}
e.preventDefault()
}, [isDragging, dragStartX, dragStartWidth, setOpen])
@@ -212,7 +212,7 @@ export function render({model, view}) {
height: "100vh", // Full viewport height
boxSizing: "border-box",
position: "relative", // Enable positioning for drag handle
- overflowX: "hidden"
+ overflowX: "hidden"
},
}}
variant={drawer_variant}
From b769c533da4989f6fd00451114a7018e163b43c0 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 3 Nov 2025 13:28:42 +0100
Subject: [PATCH 8/8] Update docs
---
examples/reference/page/Page.ipynb | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/examples/reference/page/Page.ipynb b/examples/reference/page/Page.ipynb
index 6ea953ec..91578f88 100644
--- a/examples/reference/page/Page.ipynb
+++ b/examples/reference/page/Page.ipynb
@@ -44,8 +44,9 @@
"### Sidebar\n",
"\n",
"* **`sidebar_open`** (`boolean`): Whether the sidebar is open or closed.\n",
- "* **`sidebar_width`** (`int`): Width of the sidebar.\n",
+ "* **`sidebar_resizable`** (`boolean`): Whether the sidebar is resizable.\n",
"* **`sidebar_variant`** (`Literal[\"persistent\", \"temporary\", \"permanent\", \"auto\"]`): Whether the sidebar is persistent, temporary, permanent or automatically adapts based on screen size.\n",
+ "* **`sidebar_width`** (`int`): Width of the sidebar.\n",
"\n",
"### Contextbar\n",
"\n",