diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..40448e6
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+dotenv
\ No newline at end of file
diff --git a/.python-version b/.python-version
index e4fba21..2c07333 100644
--- a/.python-version
+++ b/.python-version
@@ -1 +1 @@
-3.12
+3.11
diff --git a/docs_for_sl/button.md b/docs_for_sl/button.md
new file mode 100644
index 0000000..218c5e6
--- /dev/null
+++ b/docs_for_sl/button.md
@@ -0,0 +1,570 @@
+---
+meta:
+ title: Button
+ description: Buttons represent actions that are available to the user.
+layout: component
+---
+
+```html:preview
+Button
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => Button;
+```
+
+## Examples
+
+### Variants
+
+Use the `variant` attribute to set the button's variant.
+
+```html:preview
+Default
+Primary
+Success
+Neutral
+Warning
+Danger
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+ Default
+ Primary
+ Success
+ Neutral
+ Warning
+ Danger
+ >
+);
+```
+
+### Sizes
+
+Use the `size` attribute to change a button's size.
+
+```html:preview
+Small
+Medium
+Large
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+ Small
+ Medium
+ Large
+ >
+);
+```
+
+### Outline Buttons
+
+Use the `outline` attribute to draw outlined buttons with transparent backgrounds.
+
+```html:preview
+Default
+Primary
+Success
+Neutral
+Warning
+Danger
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Default
+
+
+ Primary
+
+
+ Success
+
+
+ Neutral
+
+
+ Warning
+
+
+ Danger
+
+ >
+);
+```
+
+### Pill Buttons
+
+Use the `pill` attribute to give buttons rounded edges.
+
+```html:preview
+Small
+Medium
+Large
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Small
+
+
+ Medium
+
+
+ Large
+
+ >
+);
+```
+
+### Circle Buttons
+
+Use the `circle` attribute to create circular icon buttons. When this attribute is set, the button expects a single `` in the default slot.
+
+```html:preview
+
+
+
+
+
+
+
+
+
+
+
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
+
+const App = () => (
+ <>
+
+
+
+
+
+
+
+
+
+ >
+);
+```
+
+### Text Buttons
+
+Use the `text` variant to create text buttons that share the same size as regular buttons but don't have backgrounds or borders.
+
+```html:preview
+Text
+Text
+Text
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Text
+
+
+ Text
+
+
+ Text
+
+ >
+);
+```
+
+### Link Buttons
+
+It's often helpful to have a button that works like a link. This is possible by setting the `href` attribute, which will make the component render an `` under the hood. This gives you all the default link behavior the browser provides (e.g. [[CMD/CTRL/SHIFT]] + [[CLICK]]) and exposes the `target` and `download` attributes.
+
+```html:preview
+Link
+New Window
+Download
+Disabled
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+ Link
+
+ New Window
+
+
+ Download
+
+
+ Disabled
+
+ >
+);
+```
+
+:::tip
+When a `target` is set, the link will receive `rel="noreferrer noopener"` for [security reasons](https://mathiasbynens.github.io/rel-noopener/).
+:::
+
+### Setting a Custom Width
+
+As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). This is useful for making buttons span the full width of their container on smaller screens.
+
+```html:preview
+Small
+Medium
+Large
+```
+
+{% raw %}
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Small
+
+
+ Medium
+
+
+ Large
+
+ >
+);
+```
+
+{% endraw %}
+
+### Prefix and Suffix Icons
+
+Use the `prefix` and `suffix` slots to add icons.
+
+```html:preview
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+
+
+
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+
+
+
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+import SlIcon from '@shoelace-style/shoelace/dist/react/icon';
+
+const App = () => (
+ <>
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+
+
+
+
+
+
+ Settings
+
+
+
+
+ Refresh
+
+
+
+
+
+ Open
+
+ >
+);
+```
+
+### Caret
+
+Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover.
+
+```html:preview
+Small
+Medium
+Large
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Small
+
+
+ Medium
+
+
+ Large
+
+ >
+);
+```
+
+### Loading
+
+Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around.
+
+```html:preview
+Default
+Primary
+Success
+Neutral
+Warning
+Danger
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Default
+
+
+ Primary
+
+
+ Success
+
+
+ Neutral
+
+
+ Warning
+
+
+ Danger
+
+ >
+);
+```
+
+### Disabled
+
+Use the `disabled` attribute to disable a button.
+
+```html:preview
+Default
+Primary
+Success
+Neutral
+Warning
+Danger
+```
+
+```jsx:react
+import SlButton from '@shoelace-style/shoelace/dist/react/button';
+
+const App = () => (
+ <>
+
+ Default
+
+
+
+ Primary
+
+
+
+ Success
+
+
+
+ Neutral
+
+
+
+ Warning
+
+
+
+ Danger
+
+ >
+);
+```
+
+### Styling Buttons
+
+This example demonstrates how to style buttons using a custom class. This is the recommended approach if you need to add additional variations. To customize an existing variation, modify the selector to target the button's `variant` attribute instead of a class (e.g. `sl-button[variant="primary"]`).
+
+```html:preview
+Pink Button
+
+
+```
+# Shoelace Button
+
+The Shoelace button component is a customizable button that can be used in various contexts within your application.
+
+## Usage
+
+```python
+hs.sl_button(label: str, variant: str = "default", size: str = "medium", key: str = None, **kwargs)
+```
+
+## Parameters
+
+- `label` (str): The text to display on the button.
+- `variant` (str, optional): The button's visual variant. Options: "default", "primary", "success", "neutral", "warning", "danger". Default is "default".
+- `size` (str, optional): The button's size. Options: "small", "medium", "large". Default is "medium".
+- `key` (str, optional): A unique identifier for the button. Default is None.
+- `**kwargs`: Additional attributes to pass to the button element.
+
+## Example
+
+```python
+hs.sl_button("Click me!", variant="primary", size="large", key="my_button")
+```
+
+This will create a large primary button with the text "Click me!".
diff --git a/hstream/components/components.py b/hstream/components/components.py
index 85b7047..3b09344 100644
--- a/hstream/components/components.py
+++ b/hstream/components/components.py
@@ -118,7 +118,7 @@ def text_input(
"input",
("hx-ext", "debug"),
("name", "new_value"),
- ("hx-post", f"/set_component_value?component_id={key}"),
+ ("hx-get", f"/set_component_value?component_id={key}"),
("hx-swap", "none"),
("type", "text"),
):
@@ -152,7 +152,7 @@ def number_input(
with self.tag(
"input",
("name", "new_value"),
- ("hx-post", f"/set_component_value?component_id={key}"),
+ ("hx-get", f"/set_component_value?component_id={key}"),
("hx-trigger", "focusout"),
("type", "number"),
("value", str(kwargs["value"])),
@@ -176,7 +176,7 @@ def select_box(
# HStream developer note: The default value logic is handled by the `@component_wrapper`
with self.doc.select(
("name", "new_value"),
- ("hx-post", f"/set_component_value?component_id={key}"),
+ ("hx-get", f"/set_component_value?component_id={key}"),
):
for value in label:
with self.tag(
@@ -250,7 +250,7 @@ def multiselect(
with self.tag(
"details",
("class", "dropdown"),
- ("name", "new_value"),
+ ("name", "new_multi_value"),
("multiple", ""),
):
with self.tag("summary", ("style", "overflow:hidden;")):
@@ -358,7 +358,7 @@ def nav(
"li",
("hx-trigger", "click"),
(
- "hx-post",
+ "hx-get",
f"/set_component_value/?component_id={key}&new_value={item}",
),
("hx-swap", "none"),
@@ -446,7 +446,7 @@ def bool_checker_format_fn(user_checkbox_value):
# so we attach the state of the checkbox here
# https://htmx.org/attributes/hx-vals/, https://github.com/bigskysoftware/htmx/issues/894
("hx-vals", "js:{new_value: event.srcElement.checked}"),
- ("hx-post", f"/set_component_value?component_id={key}"),
+ ("hx-get", f"/set_component_value?component_id={key}"),
# (
# "hx-get",
# f"/set_component_value/?component_id={key}&new_value={not value}",
@@ -481,7 +481,7 @@ def button(
("id", key),
("type", "submit") if full_width else ("type", "button"),
("hx-trigger", "click"),
- ("hx-post", f"/set_component_value?component_id={key}&new_value=true"),
+ ("hx-get", f"/set_component_value?component_id={key}&new_value=true"),
("hx-swap", "none"),
):
self.text(label)
diff --git a/hstream/components/shoelace/__init__.py b/hstream/components/shoelace/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/hstream/components/shoelace/card.py b/hstream/components/shoelace/card.py
new file mode 100644
index 0000000..3666191
--- /dev/null
+++ b/hstream/components/shoelace/card.py
@@ -0,0 +1,41 @@
+from hstream.components.components import ComponentsGeneric, component_wrapper
+
+@component_wrapper
+def sl_card(
+ self,
+ content: str,
+ key: str = None,
+ **kwargs
+) -> None:
+ """
+ Render a Shoelace card component.
+
+ Args:
+ content (str): The main content of the card.
+ key (str, optional): A unique identifier for the card. Default is None.
+ **kwargs: Additional attributes to pass to the card element.
+ - header (str, optional): The content to display in the header. Default is None.
+ - footer (str, optional): The content to display in the footer. Default is None.
+ - image_url (str, optional): The URL of an image to display at the top of the card. Default is None.
+ """
+ header: str = kwargs.get("header", None)
+ footer: str = kwargs.get("footer", None)
+ image_url: str = kwargs.get("image_url", None)
+
+ with self.tag("sl-card", ("id", key) if key else None):
+ if header:
+ with self.tag("div", ("slot", "header")):
+ self.doc.text(header)
+
+ if image_url:
+ with self.tag("img", ("slot", "image"), ("src", image_url), ("alt", "Card image")):
+ pass
+
+ self.doc.text(content)
+
+ if footer:
+ with self.tag("div", ("slot", "footer")):
+ self.doc.text(footer)
+
+ _hs_session[key] = None # Store session data for the card if needed
+ return lambda s: s # Card doesn't return any specific action value
diff --git a/hstream/components/shoelace/format_date.py b/hstream/components/shoelace/format_date.py
new file mode 100644
index 0000000..d391ac4
--- /dev/null
+++ b/hstream/components/shoelace/format_date.py
@@ -0,0 +1,32 @@
+from hstream.components.components import component_wrapper
+from typing import Optional
+
+
+@component_wrapper
+def sl_format_date(self, date: str, key: Optional[str] = None, **kwargs) -> None:
+ """
+ Render a Shoelace format-date component.
+
+ Args:
+ date (str): The date to format.
+ key (str, optional): A unique identifier for the component.
+ **kwargs: Additional attributes to pass to the format-date element.
+ Possible attributes include:
+ - locale: The locale to use when formatting the date.
+ - weekday: How to display the weekday.
+ - era: How to display the era.
+ - year: How to display the year.
+ - month: How to display the month.
+ - day: How to display the day.
+ - hour: How to display the hour.
+ - minute: How to display the minute.
+ - second: How to display the second.
+ - time_zone_name: How to display the time zone name.
+ - time_zone: The time zone to express the time in.
+ - hour_format: The hour format to use.
+ """
+ attributes = [("date", date)]
+ attributes.extend([(k.replace("_", "-"), v) for k, v in kwargs.items() if v is not None])
+
+ with self.tag("sl-format-date", *attributes):
+ pass
diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py
new file mode 100644
index 0000000..5fb0f05
--- /dev/null
+++ b/hstream/components/shoelace_components.py
@@ -0,0 +1,97 @@
+from hstream.components.components import ComponentsGeneric, component_wrapper
+from typing import List, Optional, Any
+from hstream.components.shoelace.card import sl_card
+from hstream.components.shoelace.format_date import sl_format_date
+
+class ShoelaceComponents(ComponentsGeneric):
+
+ sl_card = sl_card
+ sl_format_date = sl_format_date
+
+ @component_wrapper
+ def sl_button(
+ self, label: str, default_value: List[str] = None, key: str = None, **kwargs
+ ) -> None:
+ """
+ Render a Shoelace button component.
+
+ Args:
+ label (str): The text to display on the button.
+ variant (str, optional): The button's visual variant. Default is "default".
+ size (str, optional): The button's size. Default is "medium".
+ key (str, optional): A unique identifier for the button. Default is None.
+ **kwargs: Additional attributes to pass to the button element.
+ """
+ variant: str = kwargs.get("variant", "default")
+ size: str = kwargs.get("size", "medium")
+ with self.tag(
+ "sl-button",
+ ("variant", variant),
+ ("size", size),
+ ("hx-get", f"/set_component_value?component_id={key}&new_value=true"),
+ ("hx-trigger", "click"),
+ ):
+ self.doc.text(label)
+ _hs_session[ # noqa: F821
+ key
+ ] = False # set the button back to false after it has been clicked
+ return lambda s: True if s in ["True", "true", True] else False
+
+ @component_wrapper
+ def sl_input(
+ self, label: str, default_value: Any = None, key: Optional[str] = None, **kwargs
+ ) -> None:
+ """
+ Render a Shoelace input component.
+
+ Args:
+ label (str): The label for the input field.
+ default_value (Any, optional): The default value for the input. Default is None.
+ key (str, optional): A unique identifier for the input. Default is None.
+ **kwargs: Additional attributes to pass to the input element.
+ Possible attributes include:
+ - type: The type of input (e.g., "text", "number", "email", etc.)
+ - placeholder: Placeholder text for the input
+ - size: The size of the input field
+ - disabled: Whether the input is disabled
+ - readonly: Whether the input is read-only
+ """
+ input_type = kwargs.get("type", "text")
+ placeholder = kwargs.get("placeholder", "")
+ size = kwargs.get("size", "medium")
+ disabled = "disabled" if kwargs.get("disabled", False) else ""
+ readonly = "readonly" if kwargs.get("readonly", False) else "false"
+ with self.tag(
+ "sl-input",
+ ("name", "new_value"),
+ ("type", input_type),
+ ("label", label),
+ ("placeholder", placeholder),
+ ("size", size),
+ (disabled, ""),
+ (readonly, ""),
+ ("value", str(kwargs['value']) if kwargs['value'] is not None else ""),
+ ("hx-get", f"/set_component_value?component_id={key}"),
+ ("hx-trigger", "sl-change"),
+ ):
+ pass
+ return lambda x: x[0] if type(x) == list else x
+
+ @component_wrapper
+ def sl_multiselect(
+ self, label: List[str], default_value: List[str] = None, key: str = None, **kwargs
+ ) -> None:
+ options = [ f'{option}' for option in label]
+ assert default_value is None or all([d in label for d in default_value]), f"Default value must be in options: {default_value} not in {label} - default_value must also be itterable"
+ value = ' '.join(list(kwargs["value"] )) if default_value else ''
+
+ with self.tag(
+ "sl-select",
+ ("value", value),
+ ("name", "new_value"),
+ ("multiple", "" ),
+ ("clearable", ""),
+ ("hx-post", f"/set_component_value?component_id={key}"),
+ ("hx-trigger", "sl-hide"),
+ ):
+ self.doc.asis(''.join(options))
diff --git a/hstream/django_server/hs/views.py b/hstream/django_server/hs/views.py
index f094a1e..e3c967d 100644
--- a/hstream/django_server/hs/views.py
+++ b/hstream/django_server/hs/views.py
@@ -178,11 +178,14 @@ def run_user_code_and_return_hs_instance(file: Path, request: HttpRequest) -> hs
def set_component_value(
request: HttpRequest,
):
- # import ipdb; ipdb.set_trace()
component_id = request.GET.get("component_id")
- new_value = request.POST.get("new_value")
+ # POST for multi values
+ # we neeed to convert the request.POST to a dict to handle multiple values coming back
+ new_value = dict(request.POST).get("new_multi_value")
request_server_stop_running_user_script(request, wait=True)
+
+ # GET for single values
if new_value is None:
new_value = request.GET.get("new_value")
set_session_var(request, component_id, new_value)
diff --git a/hstream/hs.py b/hstream/hs.py
index a533135..8fa9d3d 100644
--- a/hstream/hs.py
+++ b/hstream/hs.py
@@ -3,7 +3,7 @@
# from hstream.components.text_input import text_input
from hstream.components.components import Components
from hstream.components.styling_components import StyledComponents
-
+from hstream.components.shoelace_components import ShoelaceComponents
class HSDoc(Doc):
class Tag(Doc.Tag):
@@ -13,7 +13,7 @@ def __exit__(self, tpe, value, traceback):
return res
-class hs(Components, StyledComponents):
+class hs(Components, StyledComponents, ShoelaceComponents):
def __init__(self) -> (HSDoc, HSDoc.tag, HSDoc.text):
self.doc, self.tag, self.text = HSDoc().tagtext()
diff --git a/hstream/templates/format_html.html b/hstream/templates/format_html.html
index c4a8487..a6dce5a 100644
--- a/hstream/templates/format_html.html
+++ b/hstream/templates/format_html.html
@@ -2,14 +2,16 @@
+
+
+
+
-
-
-
-
+
+
diff --git a/test.py b/test.py
new file mode 100644
index 0000000..b0bff90
--- /dev/null
+++ b/test.py
@@ -0,0 +1,10 @@
+from hstream import hs
+
+hs.markdown('test')
+
+sl_ms = hs.sl_multiselect(['test1', 'test2', 'test3'], default_value=['test2'])
+sl_ms = hs.sl_button('Shoelace button')
+
+hs.markdown(sl_ms)
+
+hs.button('Normal button')
\ No newline at end of file
diff --git a/tests/1_basic_test.py b/tests/1_basic_test.py
index 42f6201..36df385 100644
--- a/tests/1_basic_test.py
+++ b/tests/1_basic_test.py
@@ -3,12 +3,13 @@
from .conftest import write_py_script
import platform
+HEADLESS = False
def do_content_in_page(content):
with sync_playwright() as playwright:
if not platform.system() == "Darwin":
sleep(10) # wait for github server to start
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
sleep(2)
@@ -18,7 +19,7 @@ def do_content_in_page(content):
def test_blank_page():
- sleep(0)
+ sleep(2)
write_py_script(contents="from hstream import hs \nhs.markdown('Hello world')")
do_content_in_page(content="Hello world")
@@ -34,7 +35,7 @@ def test_button():
hs.markdown('pressed')
"""
)
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
assert "pressed" not in page.inner_text("body")
@@ -60,7 +61,7 @@ def test_text():
hs.markdown(text)
"""
)
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
text_input = page.locator("input")
@@ -80,7 +81,7 @@ def test_number_input():
hs.markdown(str(number))
"""
)
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
number_input = page.locator("input[type=number]")
@@ -101,7 +102,7 @@ def test_select_box():
hs.markdown(option)
"""
)
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
select_box = page.locator("select")
@@ -122,7 +123,7 @@ def test_checkbox():
hs.text_input('text')
"""
)
- browser = playwright.chromium.launch(headless=True)
+ browser = playwright.chromium.launch(headless=HEADLESS)
page = browser.new_page()
page.goto("http://127.0.0.1:9000/")
checkbox = page.locator("input[type=checkbox]")
@@ -132,3 +133,87 @@ def test_checkbox():
sleep(2)
assert "True" in page.inner_text("body")
browser.close()
+
+def test_sl_button():
+ sleep(0)
+ with sync_playwright() as playwright:
+ write_py_script(
+ contents="""
+from hstream import hs
+if hs.sl_button('Click me', variant='primary'):
+ hs.markdown('Button clicked!')
+"""
+ )
+ browser = playwright.chromium.launch(headless=HEADLESS)
+ page = browser.new_page()
+ page.goto("http://127.0.0.1:9000/")
+ button = page.locator('sl-button:has-text("Click me")')
+ assert button.get_attribute('variant') == 'primary'
+ assert "Button clicked!" not in page.inner_text("body")
+ button.click()
+ sleep(2)
+ assert "Button clicked!" in page.inner_text("body")
+ browser.close()
+
+def test_sl_input():
+ sleep(1)
+ with sync_playwright() as playwright:
+ write_py_script(
+ contents="""
+from hstream import hs
+text = hs.sl_input('Enter text', placeholder='Type here')
+hs.markdown(text)
+"""
+ )
+ browser = playwright.chromium.launch(headless=HEADLESS)
+ page = browser.new_page()
+ page.goto("http://127.0.0.1:9000/")
+ input_field = page.locator("sl-input")
+ assert input_field.get_attribute('placeholder') == 'Type here'
+ input_field.focus()
+ input_field.click()
+ input_field.type("Hello, Shoelace!")
+ input_field.press("Enter")
+ sleep(2)
+ assert "Hello, Shoelace!" in page.inner_text("body")
+ browser.close()
+
+def test_sl_format_date():
+ sleep(1)
+ with sync_playwright() as playwright:
+ write_py_script(
+ contents="""
+from hstream import hs
+hs.sl_format_date('1988-05-15', month='long', day='numeric', year='numeric')
+"""
+ )
+ browser = playwright.chromium.launch(headless=HEADLESS)
+ page = browser.new_page()
+ page.goto("http://127.0.0.1:9000/")
+ # formatted_date = page.locator("sl-format-date")
+ sleep(2)
+ assert "1988" in page.content()
+ browser.close()
+
+def test_sl_multiselect():
+ sleep(0)
+ with sync_playwright() as playwright:
+ write_py_script(
+ contents="""
+from hstream import hs
+options = ['Apple', 'Banana', 'Cherry']
+selected = hs.sl_multiselect(options, default_value=['Apple'])
+hs.markdown(str(selected))
+"""
+ )
+ browser = playwright.chromium.launch(headless=HEADLESS)
+ page = browser.new_page()
+ page.goto("http://127.0.0.1:9000/")
+ multiselect = page.locator("sl-select")
+ assert "Apple" in multiselect.inner_text()
+ multiselect.click()
+ page.locator("sl-option", has_text="Cherry").click()
+ page.keyboard.press("Escape")
+ sleep(2)
+ assert "['Apple', 'Cherry']" in page.inner_text("body")
+ browser.close()