From 752c16c8051d3ca889d20f9b665e70be965a0810 Mon Sep 17 00:00:00 2001 From: conrad Date: Fri, 6 Sep 2024 02:55:38 +0800 Subject: [PATCH 01/16] feat: shoelace multiselect --- hstream/components/shoelace_components.py | 21 +++++++++++++++++++++ hstream/django_server/hs/views.py | 5 +++-- hstream/hs.py | 4 ++-- hstream/templates/format_html.html | 3 ++- test.py | 7 +++++++ 5 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 hstream/components/shoelace_components.py create mode 100644 test.py diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py new file mode 100644 index 0000000..99c8284 --- /dev/null +++ b/hstream/components/shoelace_components.py @@ -0,0 +1,21 @@ +from hstream.components.components import ComponentsGeneric, component_wrapper +from typing import List +class ShoelaceComponents(ComponentsGeneric): + @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..03cbd0d 100644 --- a/hstream/django_server/hs/views.py +++ b/hstream/django_server/hs/views.py @@ -178,9 +178,10 @@ 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") + # we neeed to convert the request.POST to a dict to handle multiple values coming back + new_value = dict(request.POST).get("new_value") + # import ipdb; ipdb.set_trace() request_server_stop_running_user_script(request, wait=True) if new_value is None: 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..9624554 100644 --- a/hstream/templates/format_html.html +++ b/hstream/templates/format_html.html @@ -9,7 +9,8 @@ - + + diff --git a/test.py b/test.py new file mode 100644 index 0000000..1e7f2c8 --- /dev/null +++ b/test.py @@ -0,0 +1,7 @@ +from hstream import hs + +hs.markdown('test') + +sl_ms = hs.sl_multiselect(['test1', 'test2', 'test3'], default_value=['test2']) + +hs.markdown(sl_ms) \ No newline at end of file From d8ad9ef3fd0681e08307d148fcab44eb7257ad88 Mon Sep 17 00:00:00 2001 From: conrad Date: Fri, 6 Sep 2024 03:11:45 +0800 Subject: [PATCH 02/16] feat: Add button documentation --- docs_for_sl/button.md | 545 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 545 insertions(+) create mode 100644 docs_for_sl/button.md diff --git a/docs_for_sl/button.md b/docs_for_sl/button.md new file mode 100644 index 0000000..4b8a5a9 --- /dev/null +++ b/docs_for_sl/button.md @@ -0,0 +1,545 @@ +--- +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 + + +``` From 984e69c964eb81517b2488509b1b1e8831f5ccb5 Mon Sep 17 00:00:00 2001 From: "conrad (aider)" Date: Fri, 6 Sep 2024 03:11:47 +0800 Subject: [PATCH 03/16] feat: add sl_button method to ShoelaceComponents class --- docs_for_sl/button.md | 25 +++++++++++++++++++++++ hstream/components/shoelace_components.py | 24 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/docs_for_sl/button.md b/docs_for_sl/button.md index 4b8a5a9..218c5e6 100644 --- a/docs_for_sl/button.md +++ b/docs_for_sl/button.md @@ -543,3 +543,28 @@ This example demonstrates how to style buttons using a custom class. This is the } ``` +# 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/shoelace_components.py b/hstream/components/shoelace_components.py index 99c8284..23ca109 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -1,7 +1,31 @@ from hstream.components.components import ComponentsGeneric, component_wrapper from typing import List + class ShoelaceComponents(ComponentsGeneric): @component_wrapper + def sl_button( + self, label: str, variant: str = "default", size: str = "medium", 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. + """ + with self.tag( + "sl-button", + ("variant", variant), + ("size", size), + ("hx-post", f"/set_component_value?component_id={key}"), + ("hx-trigger", "click"), + **kwargs + ): + self.doc.text(label) + @component_wrapper def sl_multiselect( self, label: List[str], default_value: List[str] = None, key: str = None, **kwargs ) -> None: From 477215acaa2e105b1d582d48fa36b478976c8c7a Mon Sep 17 00:00:00 2001 From: conrad Date: Fri, 6 Sep 2024 03:23:04 +0800 Subject: [PATCH 04/16] fix: Update ShoelaceComponents.sl_button to use default_value and set button state after click --- hstream/components/shoelace_components.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py index 23ca109..c0eecd0 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -4,7 +4,7 @@ class ShoelaceComponents(ComponentsGeneric): @component_wrapper def sl_button( - self, label: str, variant: str = "default", size: str = "medium", key: str = None, **kwargs + self, label: str, default_value: List[str] = None, key: str = None, **kwargs ) -> None: """ Render a Shoelace button component. @@ -16,15 +16,20 @@ def sl_button( 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-post", f"/set_component_value?component_id={key}"), + ("hx-post", f"/set_component_value?component_id={key}&new_value=true"), ("hx-trigger", "click"), - **kwargs ): 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_multiselect( self, label: List[str], default_value: List[str] = None, key: str = None, **kwargs From 6f25ca242ceedfc927954fa8559d54e63aa5a9a1 Mon Sep 17 00:00:00 2001 From: conrad Date: Fri, 6 Sep 2024 03:25:14 +0800 Subject: [PATCH 05/16] feat: shoelace multiselect --- .envrc | 1 + test.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .envrc 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/test.py b/test.py index 1e7f2c8..b0bff90 100644 --- a/test.py +++ b/test.py @@ -3,5 +3,8 @@ 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) \ No newline at end of file +hs.markdown(sl_ms) + +hs.button('Normal button') \ No newline at end of file From 3074bf44d4c449626b17fd129458a4de7fab9fa1 Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 02:05:47 +0800 Subject: [PATCH 06/16] feat: Add sl_card component to ShoelaceComponents --- hstream/components/shoelace_components.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py index c0eecd0..1847d37 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -1,7 +1,11 @@ from hstream.components.components import ComponentsGeneric, component_wrapper from typing import List +from hstream.components.shoelace.card import sl_card class ShoelaceComponents(ComponentsGeneric): + + sl_card = sl_card + @component_wrapper def sl_button( self, label: str, default_value: List[str] = None, key: str = None, **kwargs From 066c96cabbdca0b998f31f31e0aeee2bce611241 Mon Sep 17 00:00:00 2001 From: "conrad (aider)" Date: Sat, 7 Sep 2024 02:05:49 +0800 Subject: [PATCH 07/16] feat: Add format-date component --- hstream/components/shoelace/format_date.py | 64 ++++++++++++++++++++++ hstream/components/shoelace_components.py | 2 + 2 files changed, 66 insertions(+) create mode 100644 hstream/components/shoelace/format_date.py diff --git a/hstream/components/shoelace/format_date.py b/hstream/components/shoelace/format_date.py new file mode 100644 index 0000000..a2509e9 --- /dev/null +++ b/hstream/components/shoelace/format_date.py @@ -0,0 +1,64 @@ +from hstream.components.components import component_wrapper +from typing import Optional + + +@component_wrapper +def sl_format_date( + self, + date: str, + locale: Optional[str] = None, + weekday: Optional[str] = None, + era: Optional[str] = None, + year: Optional[str] = None, + month: Optional[str] = None, + day: Optional[str] = None, + hour: Optional[str] = None, + minute: Optional[str] = None, + second: Optional[str] = None, + time_zone_name: Optional[str] = None, + time_zone: Optional[str] = None, + hour_format: Optional[str] = None, + key: Optional[str] = None, + **kwargs +) -> None: + """ + Render a Shoelace format-date component. + + Args: + date (str): The date to format. + locale (str, optional): The locale to use when formatting the date. + weekday (str, optional): How to display the weekday. + era (str, optional): How to display the era. + year (str, optional): How to display the year. + month (str, optional): How to display the month. + day (str, optional): How to display the day. + hour (str, optional): How to display the hour. + minute (str, optional): How to display the minute. + second (str, optional): How to display the second. + time_zone_name (str, optional): How to display the time zone name. + time_zone (str, optional): The time zone to express the time in. + hour_format (str, optional): The hour format to use. + key (str, optional): A unique identifier for the component. + **kwargs: Additional attributes to pass to the format-date element. + """ + attributes = [ + ("date", date), + ("locale", locale), + ("weekday", weekday), + ("era", era), + ("year", year), + ("month", month), + ("day", day), + ("hour", hour), + ("minute", minute), + ("second", second), + ("time-zone-name", time_zone_name), + ("time-zone", time_zone), + ("hour-format", hour_format), + ] + + # Filter out None values + filtered_attributes = [(k, v) for k, v in attributes if v is not None] + + with self.tag("sl-format-date", *filtered_attributes, *kwargs.items()): + pass diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py index 1847d37..63f06d3 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -1,10 +1,12 @@ from hstream.components.components import ComponentsGeneric, component_wrapper from typing import List 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( From 143945855fc0defe58113d2b305c94875118e9a8 Mon Sep 17 00:00:00 2001 From: "conrad (aider)" Date: Sat, 7 Sep 2024 02:06:52 +0800 Subject: [PATCH 08/16] refactor: Use **kwargs in sl_format_date function --- hstream/components/shoelace/format_date.py | 66 ++++++---------------- 1 file changed, 17 insertions(+), 49 deletions(-) diff --git a/hstream/components/shoelace/format_date.py b/hstream/components/shoelace/format_date.py index a2509e9..d391ac4 100644 --- a/hstream/components/shoelace/format_date.py +++ b/hstream/components/shoelace/format_date.py @@ -3,62 +3,30 @@ @component_wrapper -def sl_format_date( - self, - date: str, - locale: Optional[str] = None, - weekday: Optional[str] = None, - era: Optional[str] = None, - year: Optional[str] = None, - month: Optional[str] = None, - day: Optional[str] = None, - hour: Optional[str] = None, - minute: Optional[str] = None, - second: Optional[str] = None, - time_zone_name: Optional[str] = None, - time_zone: Optional[str] = None, - hour_format: Optional[str] = None, - key: Optional[str] = None, - **kwargs -) -> None: +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. - locale (str, optional): The locale to use when formatting the date. - weekday (str, optional): How to display the weekday. - era (str, optional): How to display the era. - year (str, optional): How to display the year. - month (str, optional): How to display the month. - day (str, optional): How to display the day. - hour (str, optional): How to display the hour. - minute (str, optional): How to display the minute. - second (str, optional): How to display the second. - time_zone_name (str, optional): How to display the time zone name. - time_zone (str, optional): The time zone to express the time in. - hour_format (str, optional): The hour format to use. 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), - ("locale", locale), - ("weekday", weekday), - ("era", era), - ("year", year), - ("month", month), - ("day", day), - ("hour", hour), - ("minute", minute), - ("second", second), - ("time-zone-name", time_zone_name), - ("time-zone", time_zone), - ("hour-format", hour_format), - ] + attributes = [("date", date)] + attributes.extend([(k.replace("_", "-"), v) for k, v in kwargs.items() if v is not None]) - # Filter out None values - filtered_attributes = [(k, v) for k, v in attributes if v is not None] - - with self.tag("sl-format-date", *filtered_attributes, *kwargs.items()): + with self.tag("sl-format-date", *attributes): pass From a2d3a581a579b2668fde0c64c85927b24e224046 Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 02:09:09 +0800 Subject: [PATCH 09/16] feat: new shoelace --- .python-version | 2 +- hstream/components/shoelace/__init__.py | 0 hstream/components/shoelace/card.py | 41 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 hstream/components/shoelace/__init__.py create mode 100644 hstream/components/shoelace/card.py 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/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 From 19e632cea56db93e2063265af98afb588697c106 Mon Sep 17 00:00:00 2001 From: "conrad (aider)" Date: Sat, 7 Sep 2024 02:15:49 +0800 Subject: [PATCH 10/16] feat: Add Shoelace text input component --- hstream/components/shoelace_components.py | 44 +++++++++++++++++++++-- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py index 63f06d3..8410786 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -1,5 +1,5 @@ from hstream.components.components import ComponentsGeneric, component_wrapper -from typing import List +from typing import List, Optional, Any from hstream.components.shoelace.card import sl_card from hstream.components.shoelace.format_date import sl_format_date @@ -36,10 +36,50 @@ def sl_button( 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 = "true" if kwargs.get("disabled", False) else "false" + readonly = "true" if kwargs.get("readonly", False) else "false" + + with self.tag( + "sl-input", + ("type", input_type), + ("label", label), + ("placeholder", placeholder), + ("size", size), + ("disabled", disabled), + ("readonly", readonly), + ("value", str(default_value) if default_value is not None else ""), + ("hx-post", f"/set_component_value?component_id={key}"), + ("hx-trigger", "sl-change"), + ): + pass + @component_wrapper def sl_multiselect( self, label: List[str], default_value: List[str] = None, key: str = None, **kwargs - ) -> None: + ) -> 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 '' From cb9d3b16e7a3445d93e8f827d235ffb01b92fa17 Mon Sep 17 00:00:00 2001 From: "conrad (aider)" Date: Sat, 7 Sep 2024 02:38:16 +0800 Subject: [PATCH 11/16] feat: add tests for Shoelace components --- tests/1_basic_test.py | 82 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/tests/1_basic_test.py b/tests/1_basic_test.py index 42f6201..4b9109c 100644 --- a/tests/1_basic_test.py +++ b/tests/1_basic_test.py @@ -132,3 +132,85 @@ 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=True) + 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(0) + 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=True) + 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.fill("Hello, Shoelace!") + input_field.press("Enter") + sleep(2) + assert "Hello, Shoelace!" in page.inner_text("body") + browser.close() + +def test_sl_format_date(): + sleep(0) + with sync_playwright() as playwright: + write_py_script( + contents=""" +from hstream import hs +hs.sl_format_date('2023-05-15', month='long', day='numeric', year='numeric') +""" + ) + browser = playwright.chromium.launch(headless=True) + page = browser.new_page() + page.goto("http://127.0.0.1:9000/") + formatted_date = page.locator("sl-format-date") + sleep(2) + assert "May 15, 2023" in page.inner_text("body") + 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=True) + 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() From 6685bed35b866bad58a0449fefa839b39b3a74e4 Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 03:03:22 +0800 Subject: [PATCH 12/16] feat: multi-value components returns handled diffrently to single value returns (i.e. multiselects) --- hstream/components/components.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) 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) From e796d23c536486ec5fb5b52a0ee23fa2283fbd4f Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 03:03:35 +0800 Subject: [PATCH 13/16] chore: cleanup --- hstream/templates/format_html.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/hstream/templates/format_html.html b/hstream/templates/format_html.html index 9624554..a6dce5a 100644 --- a/hstream/templates/format_html.html +++ b/hstream/templates/format_html.html @@ -2,13 +2,14 @@ + + + + - - - From 576d0c760eecb6abbd610361394695e3135f546e Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 03:03:48 +0800 Subject: [PATCH 14/16] feat: multi-value components returns handled diffrently to single value returns (i.e. multiselects) --- hstream/django_server/hs/views.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hstream/django_server/hs/views.py b/hstream/django_server/hs/views.py index 03cbd0d..e3c967d 100644 --- a/hstream/django_server/hs/views.py +++ b/hstream/django_server/hs/views.py @@ -179,11 +179,13 @@ def set_component_value( request: HttpRequest, ): component_id = request.GET.get("component_id") + # 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_value") - # import ipdb; ipdb.set_trace() + 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) From f6bf626e56f8cd03eef7bbf87fe5f341aa6ed87b Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 03:03:57 +0800 Subject: [PATCH 15/16] feat: multi-value components returns handled diffrently to single value returns (i.e. multiselects) --- hstream/components/shoelace_components.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/hstream/components/shoelace_components.py b/hstream/components/shoelace_components.py index 8410786..5fb0f05 100644 --- a/hstream/components/shoelace_components.py +++ b/hstream/components/shoelace_components.py @@ -28,7 +28,7 @@ def sl_button( "sl-button", ("variant", variant), ("size", size), - ("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-trigger", "click"), ): self.doc.text(label) @@ -59,22 +59,23 @@ def sl_input( input_type = kwargs.get("type", "text") placeholder = kwargs.get("placeholder", "") size = kwargs.get("size", "medium") - disabled = "true" if kwargs.get("disabled", False) else "false" - readonly = "true" if kwargs.get("readonly", False) else "false" - + 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", disabled), - ("readonly", readonly), - ("value", str(default_value) if default_value is not None else ""), - ("hx-post", f"/set_component_value?component_id={key}"), + (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( From 1719742db1eb1dd19afdbfb17950b773785030dc Mon Sep 17 00:00:00 2001 From: conrad Date: Sat, 7 Sep 2024 03:04:08 +0800 Subject: [PATCH 16/16] feat: shoelace tests --- tests/1_basic_test.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/1_basic_test.py b/tests/1_basic_test.py index 4b9109c..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]") @@ -143,7 +144,7 @@ def test_sl_button(): hs.markdown('Button clicked!') """ ) - browser = playwright.chromium.launch(headless=True) + 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")') @@ -155,7 +156,7 @@ def test_sl_button(): browser.close() def test_sl_input(): - sleep(0) + sleep(1) with sync_playwright() as playwright: write_py_script( contents=""" @@ -164,32 +165,34 @@ def test_sl_input(): 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/") input_field = page.locator("sl-input") assert input_field.get_attribute('placeholder') == 'Type here' - input_field.fill("Hello, Shoelace!") + 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(0) + sleep(1) with sync_playwright() as playwright: write_py_script( contents=""" from hstream import hs -hs.sl_format_date('2023-05-15', month='long', day='numeric', year='numeric') +hs.sl_format_date('1988-05-15', month='long', day='numeric', year='numeric') """ ) - browser = playwright.chromium.launch(headless=True) + 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") + # formatted_date = page.locator("sl-format-date") sleep(2) - assert "May 15, 2023" in page.inner_text("body") + assert "1988" in page.content() browser.close() def test_sl_multiselect(): @@ -203,7 +206,7 @@ def test_sl_multiselect(): hs.markdown(str(selected)) """ ) - browser = playwright.chromium.launch(headless=True) + browser = playwright.chromium.launch(headless=HEADLESS) page = browser.new_page() page.goto("http://127.0.0.1:9000/") multiselect = page.locator("sl-select")