Browse Source

Merge pull request #1 from phoskee/AutoCompleteInput

Jacopo 2 years ago
parent
commit
b6127ad03b
47 changed files with 627 additions and 270 deletions
  1. 3 3
      CITATION.cff
  2. 3 1
      CONTRIBUTING.md
  3. 1 1
      fly.dockerfile
  4. 1 1
      fly.toml
  5. 1 1
      nicegui/element.py
  6. 2 1
      nicegui/elements/button.py
  7. 2 1
      nicegui/elements/checkbox.py
  8. 2 1
      nicegui/elements/color_input.py
  9. 21 0
      nicegui/elements/dark_mode.js
  10. 46 0
      nicegui/elements/dark_mode.py
  11. 2 1
      nicegui/elements/date.py
  12. 2 1
      nicegui/elements/expansion.py
  13. 24 2
      nicegui/elements/input.py
  14. 2 1
      nicegui/elements/knob.py
  15. 30 0
      nicegui/elements/mixins/disableable_element.py
  16. 29 2
      nicegui/elements/number.py
  17. 2 1
      nicegui/elements/radio.py
  18. 2 1
      nicegui/elements/select.py
  19. 2 1
      nicegui/elements/slider.py
  20. 3 1
      nicegui/elements/splitter.py
  21. 2 1
      nicegui/elements/switch.py
  22. 5 5
      nicegui/elements/tabs.py
  23. 2 1
      nicegui/elements/time.py
  24. 2 1
      nicegui/elements/toggle.py
  25. 27 0
      nicegui/elements/upload.js
  26. 6 3
      nicegui/elements/upload.py
  27. 11 4
      nicegui/functions/refreshable.py
  28. 14 6
      nicegui/native_mode.py
  29. 6 10
      nicegui/nicegui.py
  30. 1 1
      nicegui/page.py
  31. 1 1
      nicegui/templates/index.html
  32. 1 0
      nicegui/ui.py
  33. 129 126
      poetry.lock
  34. 1 1
      pyproject.toml
  35. 0 14
      tests/input.py
  36. 17 0
      tests/test_button.py
  37. 36 0
      tests/test_dark_mode.py
  38. 11 0
      tests/test_input.py
  39. 7 0
      tests/test_label.py
  40. 13 0
      tests/test_number.py
  41. 37 0
      tests/test_upload.py
  42. 52 63
      website/documentation.py
  43. 13 0
      website/more_documentation/dark_mode_documentation.py
  44. 10 0
      website/more_documentation/input_documentation.py
  45. 10 0
      website/more_documentation/slider_documentation.py
  46. 12 0
      website/more_documentation/table_documentation.py
  47. 21 12
      website/more_documentation/timer_documentation.py

+ 3 - 3
CITATION.cff

@@ -8,7 +8,7 @@ authors:
   given-names: Rodja
   given-names: Rodja
   orcid: https://orcid.org/0009-0009-4735-6227
   orcid: https://orcid.org/0009-0009-4735-6227
 title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
 title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
-version: v1.2.8
-date-released: '2023-04-17'
+version: v1.2.9
+date-released: '2023-04-21'
 url: https://github.com/zauberzeug/nicegui
 url: https://github.com/zauberzeug/nicegui
-doi: 10.5281/zenodo.7835809
+doi: 10.5281/zenodo.7852795

+ 3 - 1
CONTRIBUTING.md

@@ -133,7 +133,9 @@ We are happy to merge pull requests with new examples which show new concepts, i
 
 
 ## Pull requests
 ## Pull requests
 
 
-To get started, fork the repository on GitHub, make your changes, and open a pull request (PR) with a detailed description of the changes you've made.
+To get started, fork the repository on GitHub, clone it somewhere on your filesystem, commit and push your changes,
+and then open a pull request (PR) with a detailed description of the changes you've made
+(the PR button is shown on the GitHub website of your forked repository).
 
 
 When submitting a PR, please make sure that the code follows the existing coding style and that all tests are passing.
 When submitting a PR, please make sure that the code follows the existing coding style and that all tests are passing.
 If you're adding a new feature, please include tests that cover the new functionality.
 If you're adding a new feature, please include tests that cover the new functionality.

+ 1 - 1
fly.dockerfile

@@ -2,7 +2,7 @@ FROM python:3.11-slim
 
 
 LABEL maintainer="Zauberzeug GmbH <nicegui@zauberzeug.com>"
 LABEL maintainer="Zauberzeug GmbH <nicegui@zauberzeug.com>"
 
 
-RUN pip install itsdangerous prometheus_client isort docutils
+RUN pip install itsdangerous prometheus_client isort docutils pandas
 
 
 WORKDIR /app
 WORKDIR /app
 
 

+ 1 - 1
fly.toml

@@ -12,7 +12,7 @@ processes = []
 [deploy]
 [deploy]
 # boot a single, new VM with the new release, verify its health, then
 # boot a single, new VM with the new release, verify its health, then
 # One by one, each running VM is taken down and replaced by the new release VM
 # One by one, each running VM is taken down and replaced by the new release VM
-strategy = "canary" 
+strategy = "rolling" 
 
 
 
 
 [experimental]
 [experimental]

+ 1 - 1
nicegui/element.py

@@ -41,7 +41,7 @@ class Element(Visibility):
         self._style: Dict[str, str] = {}
         self._style: Dict[str, str] = {}
         self._props: Dict[str, Any] = {}
         self._props: Dict[str, Any] = {}
         self._event_listeners: Dict[str, EventListener] = {}
         self._event_listeners: Dict[str, EventListener] = {}
-        self._text: str = ''
+        self._text: Optional[str] = None
         self.slots: Dict[str, Slot] = {}
         self.slots: Dict[str, Slot] = {}
         self.default_slot = self.add_slot('default')
         self.default_slot = self.add_slot('default')
 
 

+ 2 - 1
nicegui/elements/button.py

@@ -2,10 +2,11 @@ from typing import Callable, Optional
 
 
 from ..colors import set_background_color
 from ..colors import set_background_color
 from ..events import ClickEventArguments, handle_event
 from ..events import ClickEventArguments, handle_event
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
 
 
 
 
-class Button(TextElement):
+class Button(TextElement, DisableableElement):
 
 
     def __init__(self,
     def __init__(self,
                  text: str = '', *,
                  text: str = '', *,

+ 2 - 1
nicegui/elements/checkbox.py

@@ -1,10 +1,11 @@
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Checkbox(TextElement, ValueElement):
+class Checkbox(TextElement, ValueElement, DisableableElement):
 
 
     def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
     def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
         """Checkbox
         """Checkbox

+ 2 - 1
nicegui/elements/color_input.py

@@ -3,10 +3,11 @@ from typing import Callable, Optional
 from nicegui import ui
 from nicegui import ui
 
 
 from .color_picker import ColorPicker
 from .color_picker import ColorPicker
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class ColorInput(ValueElement):
+class ColorInput(ValueElement, DisableableElement):
     LOOPBACK = False
     LOOPBACK = False
 
 
     def __init__(self, label: Optional[str] = None, *,
     def __init__(self, label: Optional[str] = None, *,

+ 21 - 0
nicegui/elements/dark_mode.js

@@ -0,0 +1,21 @@
+export default {
+  mounted() {
+    this.update();
+  },
+  updated() {
+    this.update();
+  },
+  methods: {
+    update() {
+      Quasar.Dark.set(this.value === null ? "auto" : this.value);
+      if (window.tailwind) {
+        tailwind.config.darkMode = this.auto ? "media" : "class";
+        if (this.value) document.body.classList.add("dark");
+        else document.body.classList.remove("dark");
+      }
+    },
+  },
+  props: {
+    value: Boolean,
+  },
+};

+ 46 - 0
nicegui/elements/dark_mode.py

@@ -0,0 +1,46 @@
+from typing import Optional
+
+from ..dependencies import register_component
+from .mixins.value_element import ValueElement
+
+register_component('dark_mode', __file__, 'dark_mode.js')
+
+
+class DarkMode(ValueElement):
+    VALUE_PROP = 'value'
+
+    def __init__(self, value: Optional[bool] = False) -> None:
+        """Dark mode
+
+        You can use this element to enable, disable or toggle dark mode on the page.
+        The value `None` represents auto mode, which uses the client's system preference.
+
+        Note that this element overrides the `dark` parameter of the `ui.run` function and page decorators.
+
+        :param value: Whether dark mode is enabled. If None, dark mode is set to auto.
+        """
+        super().__init__(tag='dark_mode', value=value, on_value_change=None)
+
+    def enable(self) -> None:
+        """Enable dark mode."""
+        self.value = True
+
+    def disable(self) -> None:
+        """Disable dark mode."""
+        self.value = False
+
+    def toggle(self) -> None:
+        """Toggle dark mode.
+
+        This method will raise a ValueError if dark mode is set to auto.
+        """
+        if self.value is None:
+            raise ValueError('Cannot toggle dark mode when it is set to auto.')
+        self.value = not self.value
+
+    def auto(self) -> None:
+        """Set dark mode to auto.
+
+        This will use the client's system preference.
+        """
+        self.value = None

+ 2 - 1
nicegui/elements/date.py

@@ -1,9 +1,10 @@
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Date(ValueElement):
+class Date(ValueElement, DisableableElement):
     EVENT_ARGS = None
     EVENT_ARGS = None
 
 
     def __init__(self,
     def __init__(self,

+ 2 - 1
nicegui/elements/expansion.py

@@ -1,9 +1,10 @@
 from typing import Optional
 from typing import Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Expansion(ValueElement):
+class Expansion(ValueElement, DisableableElement):
 
 
     def __init__(self, text: Optional[str] = None, *, icon: Optional[str] = None, value: bool = False) -> None:
     def __init__(self, text: Optional[str] = None, *, icon: Optional[str] = None, value: bool = False) -> None:
         '''Expansion Element
         '''Expansion Element

+ 24 - 2
nicegui/elements/input.py

@@ -1,10 +1,11 @@
 from typing import Any, Callable, Dict, Optional
 from typing import Any, Callable, Dict, Optional
 
 
 from .icon import Icon
 from .icon import Icon
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Input(ValueElement):
+class Input(ValueElement, DisableableElement):
     LOOPBACK = False
     LOOPBACK = False
 
 
     def __init__(self,
     def __init__(self,
@@ -14,6 +15,7 @@ class Input(ValueElement):
                  password: bool = False,
                  password: bool = False,
                  password_toggle_button: bool = False,
                  password_toggle_button: bool = False,
                  on_change: Optional[Callable] = None,
                  on_change: Optional[Callable] = None,
+                 autocomplete: Optional[list] = None,
                  validation: Dict[str, Callable] = {}) -> None:
                  validation: Dict[str, Callable] = {}) -> None:
         """Text Input
         """Text Input
 
 
@@ -31,7 +33,8 @@ class Input(ValueElement):
         :param value: the current value of the text input
         :param value: the current value of the text input
         :param password: whether to hide the input (default: False)
         :param password: whether to hide the input (default: False)
         :param password_toggle_button: whether to show a button to toggle the password visibility (default: False)
         :param password_toggle_button: whether to show a button to toggle the password visibility (default: False)
-        :param on_change: callback to execute when the input is confirmed by leaving the focus
+        :param on_change: callback to execute when the value changes
+        :param autocomplete: options for autocompletition
         :param validation: dictionary of validation rules, e.g. ``{'Too short!': lambda value: len(value) < 3}``
         :param validation: dictionary of validation rules, e.g. ``{'Too short!': lambda value: len(value) < 3}``
         """
         """
         super().__init__(tag='q-input', value=value, on_value_change=on_change)
         super().__init__(tag='q-input', value=value, on_value_change=on_change)
@@ -51,6 +54,25 @@ class Input(ValueElement):
 
 
         self.validation = validation
         self.validation = validation
 
 
+        if autocomplete is not None:
+            def AutoCompleteInput():
+                if len(self.value) > 0:
+                    for item in autocomplete:
+                        if item.startswith(self.value):
+                            self.props(f'shadow-text="{item[len(self.value):]}"')
+                            wordcomplete = item
+                            return wordcomplete
+                else:
+                    self.props(f'shadow-text=" "')
+
+            def CompleteInput():
+                word = AutoCompleteInput()
+                self.set_value(word)
+                self.props(f'shadow-text=" "')
+
+            self.on("keyup", AutoCompleteInput)
+            self.on("keydown.tab", CompleteInput)
+
     def on_value_change(self, value: Any) -> None:
     def on_value_change(self, value: Any) -> None:
         super().on_value_change(value)
         super().on_value_change(value)
         for message, check in self.validation.items():
         for message, check in self.validation.items():

+ 2 - 1
nicegui/elements/knob.py

@@ -2,10 +2,11 @@ from typing import Optional
 
 
 from ..colors import set_text_color
 from ..colors import set_text_color
 from .label import Label
 from .label import Label
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Knob(ValueElement):
+class Knob(ValueElement, DisableableElement):
 
 
     def __init__(self,
     def __init__(self,
                  value: float = 0.0,
                  value: float = 0.0,

+ 30 - 0
nicegui/elements/mixins/disableable_element.py

@@ -0,0 +1,30 @@
+from ...binding import BindableProperty
+from ...element import Element
+
+
+class DisableableElement(Element):
+    enabled = BindableProperty(on_change=lambda sender, value: sender.on_enabled_change(value))
+
+    def __init__(self, **kwargs) -> None:
+        super().__init__(**kwargs)
+        self.enabled = True
+
+    def enable(self) -> None:
+        """Enable the element."""
+        self.enabled = True
+
+    def disable(self) -> None:
+        """Disable the element."""
+        self.enabled = False
+
+    def set_enabled(self, value: bool) -> None:
+        """Set the enabled state of the element."""
+        self.enabled = value
+
+    def on_enabled_change(self, enabled: bool) -> None:
+        """Called when the element is enabled or disabled.
+
+        :param enabled: The new state.
+        """
+        self._props['disable'] = not enabled
+        self.update()

+ 29 - 2
nicegui/elements/number.py

@@ -1,15 +1,21 @@
 from typing import Any, Callable, Dict, Optional
 from typing import Any, Callable, Dict, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Number(ValueElement):
+class Number(ValueElement, DisableableElement):
     LOOPBACK = False
     LOOPBACK = False
 
 
     def __init__(self,
     def __init__(self,
                  label: Optional[str] = None, *,
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = None,
                  placeholder: Optional[str] = None,
                  value: Optional[float] = None,
                  value: Optional[float] = None,
+                 min: Optional[float] = None,
+                 max: Optional[float] = None,
+                 step: Optional[float] = None,
+                 prefix: Optional[str] = None,
+                 suffix: Optional[str] = None,
                  format: Optional[str] = None,
                  format: Optional[str] = None,
                  on_change: Optional[Callable] = None,
                  on_change: Optional[Callable] = None,
                  validation: Dict[str, Callable] = {}) -> None:
                  validation: Dict[str, Callable] = {}) -> None:
@@ -23,6 +29,11 @@ class Number(ValueElement):
         :param label: displayed name for the number input
         :param label: displayed name for the number input
         :param placeholder: text to show if no value is entered
         :param placeholder: text to show if no value is entered
         :param value: the initial value of the field
         :param value: the initial value of the field
+        :param min: the minimum value allowed
+        :param max: the maximum value allowed
+        :param step: the step size for the stepper buttons
+        :param prefix: a prefix to prepend to the displayed value
+        :param suffix: a suffix to append to the displayed value
         :param format: a string like "%.2f" to format the displayed value
         :param format: a string like "%.2f" to format the displayed value
         :param on_change: callback to execute when the input is confirmed by leaving the focus
         :param on_change: callback to execute when the input is confirmed by leaving the focus
         :param validation: dictionary of validation rules, e.g. ``{'Too small!': lambda value: value < 3}``
         :param validation: dictionary of validation rules, e.g. ``{'Too small!': lambda value: value < 3}``
@@ -34,8 +45,24 @@ class Number(ValueElement):
             self._props['label'] = label
             self._props['label'] = label
         if placeholder is not None:
         if placeholder is not None:
             self._props['placeholder'] = placeholder
             self._props['placeholder'] = placeholder
+        if min is not None:
+            self._props['min'] = min
+        if max is not None:
+            self._props['max'] = max
+        if step is not None:
+            self._props['step'] = step
+        if prefix is not None:
+            self._props['prefix'] = prefix
+        if suffix is not None:
+            self._props['suffix'] = suffix
         self.validation = validation
         self.validation = validation
-        self.on('blur', self.update)  # NOTE: to apply format (#736)
+        self.on('blur', self.sanitize)
+
+    def sanitize(self) -> None:
+        value = float(self.value or 0)
+        value = max(value, self._props.get('min', -float('inf')))
+        value = min(value, self._props.get('max', float('inf')))
+        self.set_value(self.format % value if self.format else str(value))
 
 
     def on_value_change(self, value: Any) -> None:
     def on_value_change(self, value: Any) -> None:
         super().on_value_change(value)
         super().on_value_change(value)

+ 2 - 1
nicegui/elements/radio.py

@@ -1,9 +1,10 @@
 from typing import Any, Callable, Dict, List, Optional, Union
 from typing import Any, Callable, Dict, List, Optional, Union
 
 
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
+from .mixins.disableable_element import DisableableElement
 
 
 
 
-class Radio(ChoiceElement):
+class Radio(ChoiceElement, DisableableElement):
 
 
     def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None):
     def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None):
         """Radio Selection
         """Radio Selection

+ 2 - 1
nicegui/elements/select.py

@@ -5,11 +5,12 @@ from typing import Any, Callable, Dict, List, Optional, Union
 from nicegui.dependencies import register_component
 from nicegui.dependencies import register_component
 
 
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
+from .mixins.disableable_element import DisableableElement
 
 
 register_component('select', __file__, 'select.js')
 register_component('select', __file__, 'select.js')
 
 
 
 
-class Select(ChoiceElement):
+class Select(ChoiceElement, DisableableElement):
 
 
     def __init__(self, options: Union[List, Dict], *,
     def __init__(self, options: Union[List, Dict], *,
                  label: Optional[str] = None,
                  label: Optional[str] = None,

+ 2 - 1
nicegui/elements/slider.py

@@ -1,9 +1,10 @@
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Slider(ValueElement):
+class Slider(ValueElement, DisableableElement):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  min: float,
                  min: float,

+ 3 - 1
nicegui/elements/splitter.py

@@ -1,9 +1,11 @@
 from typing import Callable, Optional, Tuple
 from typing import Callable, Optional, Tuple
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Splitter(ValueElement):
+class Splitter(ValueElement, DisableableElement):
+
     def __init__(self, *,
     def __init__(self, *,
                  horizontal: Optional[bool] = False,
                  horizontal: Optional[bool] = False,
                  reverse: Optional[bool] = False,
                  reverse: Optional[bool] = False,

+ 2 - 1
nicegui/elements/switch.py

@@ -1,10 +1,11 @@
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 from .mixins.text_element import TextElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Switch(TextElement, ValueElement):
+class Switch(TextElement, ValueElement, DisableableElement):
 
 
     def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
     def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
         """Switch
         """Switch

+ 5 - 5
nicegui/elements/tabs.py

@@ -1,7 +1,7 @@
 from typing import Any, Callable, Optional
 from typing import Any, Callable, Optional
 
 
 from .. import globals
 from .. import globals
-from ..element import Element
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
@@ -22,7 +22,7 @@ class Tabs(ValueElement):
         self.panels: Optional[TabPanels] = None
         self.panels: Optional[TabPanels] = None
 
 
 
 
-class Tab(Element):
+class Tab(DisableableElement):
 
 
     def __init__(self, name: str, label: Optional[str] = None, icon: Optional[str] = None) -> None:
     def __init__(self, name: str, label: Optional[str] = None, icon: Optional[str] = None) -> None:
         """Tab
         """Tab
@@ -34,7 +34,7 @@ class Tab(Element):
         :param label: label of the tab (default: `None`, meaning the same as `name`)
         :param label: label of the tab (default: `None`, meaning the same as `name`)
         :param icon: icon of the tab (default: `None`)
         :param icon: icon of the tab (default: `None`)
         """
         """
-        super().__init__('q-tab')
+        super().__init__(tag='q-tab')
         self._props['name'] = name
         self._props['name'] = name
         self._props['label'] = label if label is not None else name
         self._props['label'] = label if label is not None else name
         if icon:
         if icon:
@@ -65,7 +65,7 @@ class TabPanels(ValueElement):
         self._props['animated'] = animated
         self._props['animated'] = animated
 
 
 
 
-class TabPanel(Element):
+class TabPanel(DisableableElement):
 
 
     def __init__(self, name: str) -> None:
     def __init__(self, name: str) -> None:
         """Tab Panel
         """Tab Panel
@@ -75,5 +75,5 @@ class TabPanel(Element):
 
 
         :param name: name of the tab panel (the value of the `TabPanels` element)
         :param name: name of the tab panel (the value of the `TabPanels` element)
         """
         """
-        super().__init__('q-tab-panel')
+        super().__init__(tag='q-tab-panel')
         self._props['name'] = name
         self._props['name'] = name

+ 2 - 1
nicegui/elements/time.py

@@ -1,9 +1,10 @@
 from typing import Callable, Optional
 from typing import Callable, Optional
 
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 from .mixins.value_element import ValueElement
 
 
 
 
-class Time(ValueElement):
+class Time(ValueElement, DisableableElement):
 
 
     def __init__(self,
     def __init__(self,
                  value: Optional[str] = None,
                  value: Optional[str] = None,

+ 2 - 1
nicegui/elements/toggle.py

@@ -1,9 +1,10 @@
 from typing import Any, Callable, Dict, List, Optional, Union
 from typing import Any, Callable, Dict, List, Optional, Union
 
 
 from .choice_element import ChoiceElement
 from .choice_element import ChoiceElement
+from .mixins.disableable_element import DisableableElement
 
 
 
 
-class Toggle(ChoiceElement):
+class Toggle(ChoiceElement, DisableableElement):
 
 
     def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None) -> None:
     def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None) -> None:
         """Toggle
         """Toggle

+ 27 - 0
nicegui/elements/upload.js

@@ -0,0 +1,27 @@
+export default {
+  template: `
+    <q-uploader ref="uploader" :url="computed_url">
+        <template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
+            <slot :name="slot" v-bind="slotProps || {}" />
+        </template>
+    </q-uploader>
+  `,
+  mounted() {
+    setTimeout(() => {
+      this.computed_url = (window.path_prefix || "") + this.url;
+    }, 0); // NOTE: wait for window.path_prefix to be set in app.mounted()
+  },
+  methods: {
+    reset() {
+      this.$refs.uploader.reset();
+    },
+  },
+  props: {
+    url: String,
+  },
+  data: function () {
+    return {
+      computed_url: this.url,
+    };
+  },
+};

+ 6 - 3
nicegui/elements/upload.py

@@ -2,12 +2,15 @@ from typing import Callable, Optional
 
 
 from fastapi import Request, Response
 from fastapi import Request, Response
 
 
-from ..element import Element
+from ..dependencies import register_component
 from ..events import EventArguments, UploadEventArguments, handle_event
 from ..events import EventArguments, UploadEventArguments, handle_event
 from ..nicegui import app
 from ..nicegui import app
+from .mixins.disableable_element import DisableableElement
 
 
+register_component('upload', __file__, 'upload.js')
 
 
-class Upload(Element):
+
+class Upload(DisableableElement):
 
 
     def __init__(self, *,
     def __init__(self, *,
                  multiple: bool = False,
                  multiple: bool = False,
@@ -32,7 +35,7 @@ class Upload(Element):
         :param label: label for the uploader (default: `''`)
         :param label: label for the uploader (default: `''`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         :param auto_upload: automatically upload files when they are selected (default: `False`)
         """
         """
-        super().__init__('q-uploader')
+        super().__init__(tag='upload')
         self._props['multiple'] = multiple
         self._props['multiple'] = multiple
         self._props['label'] = label
         self._props['label'] = label
         self._props['auto-upload'] = auto_upload
         self._props['auto-upload'] = auto_upload

+ 11 - 4
nicegui/functions/refreshable.py

@@ -1,5 +1,7 @@
 from typing import Callable, List
 from typing import Callable, List
 
 
+from typing_extensions import Self
+
 from ..element import Element
 from ..element import Element
 
 
 
 
@@ -8,19 +10,24 @@ class refreshable:
     def __init__(self, func: Callable) -> None:
     def __init__(self, func: Callable) -> None:
         """Refreshable UI functions
         """Refreshable UI functions
 
 
-        The `@refreshable` decorator allows you to create functions that have a `refresh` method.
+        The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
         This method will automatically delete all elements created by the function and recreate them.
         This method will automatically delete all elements created by the function and recreate them.
         """
         """
         self.func = func
         self.func = func
+        self.instance = None
         self.containers: List[Element] = []
         self.containers: List[Element] = []
 
 
-    def __call__(self, *args, **kwargs) -> None:
+    def __get__(self, instance, _) -> Self:
+        self.instance = instance
+        return self
+
+    def __call__(self) -> None:
         with Element('div') as container:
         with Element('div') as container:
-            self.func(*args, **kwargs)
+            self.func() if self.instance is None else self.func(self.instance)
         self.containers.append(container)
         self.containers.append(container)
 
 
     def refresh(self) -> None:
     def refresh(self) -> None:
         for container in self.containers:
         for container in self.containers:
             container.clear()
             container.clear()
             with container:
             with container:
-                self.func()
+                self.func() if self.instance is None else self.func(self.instance)

+ 14 - 6
nicegui/native_mode.py

@@ -1,6 +1,7 @@
 import _thread
 import _thread
 import multiprocessing
 import multiprocessing
 import socket
 import socket
+import sys
 import tempfile
 import tempfile
 import time
 import time
 import warnings
 import warnings
@@ -8,10 +9,13 @@ from threading import Thread
 
 
 from . import globals, helpers
 from . import globals, helpers
 
 
-with warnings.catch_warnings():
-    # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
-    warnings.filterwarnings('ignore', category=DeprecationWarning)
-    import webview
+try:
+    with warnings.catch_warnings():
+        # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
+        warnings.filterwarnings('ignore', category=DeprecationWarning)
+        import webview
+except ModuleNotFoundError:
+    pass
 
 
 
 
 def open_window(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:
 def open_window(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:
@@ -21,8 +25,12 @@ def open_window(host: str, port: int, title: str, width: int, height: int, fulls
     window_kwargs = dict(url=f'http://{host}:{port}', title=title, width=width, height=height, fullscreen=fullscreen)
     window_kwargs = dict(url=f'http://{host}:{port}', title=title, width=width, height=height, fullscreen=fullscreen)
     window_kwargs.update(globals.app.native.window_args)
     window_kwargs.update(globals.app.native.window_args)
 
 
-    webview.create_window(**window_kwargs)
-    webview.start(storage_path=tempfile.mkdtemp(), **globals.app.native.start_args)
+    try:
+        webview.create_window(**window_kwargs)
+        webview.start(storage_path=tempfile.mkdtemp(), **globals.app.native.start_args)
+    except NameError:
+        print('Native mode is not supported in this configuration. Please install pywebview to use it.')
+        sys.exit(1)
 
 
 
 
 def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:
 def activate(host: str, port: int, title: str, width: int, height: int, fullscreen: bool) -> None:

+ 6 - 10
nicegui/nicegui.py

@@ -8,6 +8,7 @@ from typing import Dict, Optional
 from fastapi import HTTPException, Request
 from fastapi import HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse, Response
 from fastapi.responses import FileResponse, Response
+from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 from fastapi_socketio import SocketManager
 
 
 from nicegui import json
 from nicegui import json
@@ -28,6 +29,11 @@ socket_manager = SocketManager(app=app, mount_location='/_nicegui_ws/', json=jso
 globals.sio = sio = app.sio
 globals.sio = sio = app.sio
 
 
 app.add_middleware(GZipMiddleware)
 app.add_middleware(GZipMiddleware)
+static_files = StaticFiles(
+    directory=(Path(__file__).parent / 'static').resolve(),
+    follow_symlink=True,
+)
+app.mount(f'/_nicegui/{__version__}/static', static_files, name='static')
 
 
 globals.index_client = Client(page('/'), shared=True).__enter__()
 globals.index_client = Client(page('/'), shared=True).__enter__()
 
 
@@ -37,16 +43,6 @@ def index(request: Request) -> Response:
     return globals.index_client.build_response(request)
     return globals.index_client.build_response(request)
 
 
 
 
-@app.get(f'/_nicegui/{__version__}' + '/static/{name}')
-def get_static(name: str):
-    return FileResponse(Path(__file__).parent / 'static' / name)
-
-
-@app.get(f'/_nicegui/{__version__}' + '/static/fonts/{name}')
-def get_static(name: str):
-    return FileResponse(Path(__file__).parent / 'static' / 'fonts' / name)
-
-
 @app.get(f'/_nicegui/{__version__}' + '/dependencies/{id}/{name}')
 @app.get(f'/_nicegui/{__version__}' + '/dependencies/{id}/{name}')
 def get_dependencies(id: int, name: str):
 def get_dependencies(id: int, name: str):
     if id in js_dependencies and js_dependencies[id].path.exists() and js_dependencies[id].path.name == name:
     if id in js_dependencies and js_dependencies[id].path.exists() and js_dependencies[id].path.name == name:

+ 1 - 1
nicegui/page.py

@@ -34,7 +34,7 @@ class page:
         :param favicon: optional relative filepath or absolute URL to a favicon (default: `None`, NiceGUI icon will be used)
         :param favicon: optional relative filepath or absolute URL to a favicon (default: `None`, NiceGUI icon will be used)
         :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
         :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
         :param response_timeout: maximum time for the decorated function to build the page (default: 3.0)
         :param response_timeout: maximum time for the decorated function to build the page (default: 3.0)
-        :param **kwargs: additional keyword arguments passed to FastAPI's @app.get method
+        :param kwargs: additional keyword arguments passed to FastAPI's @app.get method
         """
         """
         self.path = path
         self.path = path
         self.title = title
         self.title = title

+ 1 - 1
nicegui/templates/index.html

@@ -104,7 +104,7 @@
               }));
               }));
             }
             }
             const children = data.ids.map(id => renderRecursively(elements, id));
             const children = data.ids.map(id => renderRecursively(elements, id));
-            if (name === 'default' && element.text) {
+            if (name === 'default' && element.text !== null) {
               children.unshift(element.text);
               children.unshift(element.text);
             }
             }
             return [ ...rendered, ...children]
             return [ ...rendered, ...children]

+ 1 - 0
nicegui/ui.py

@@ -16,6 +16,7 @@ from .elements.color_input import ColorInput as color_input
 from .elements.color_picker import ColorPicker as color_picker
 from .elements.color_picker import ColorPicker as color_picker
 from .elements.colors import Colors as colors
 from .elements.colors import Colors as colors
 from .elements.column import Column as column
 from .elements.column import Column as column
+from .elements.dark_mode import DarkMode as dark_mode
 from .elements.date import Date as date
 from .elements.date import Date as date
 from .elements.dialog import Dialog as dialog
 from .elements.dialog import Dialog as dialog
 from .elements.expansion import Expansion as expansion
 from .elements.expansion import Expansion as expansion

+ 129 - 126
poetry.lock

@@ -65,22 +65,25 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "attrs"
 name = "attrs"
-version = "22.2.0"
+version = "23.1.0"
 description = "Classes Without Boilerplate"
 description = "Classes Without Boilerplate"
 category = "dev"
 category = "dev"
 optional = false
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 files = [
 files = [
-    {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"},
-    {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"},
+    {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"},
+    {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"},
 ]
 ]
 
 
+[package.dependencies]
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
 [package.extras]
 [package.extras]
-cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"]
-dev = ["attrs[docs,tests]"]
-docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"]
-tests = ["attrs[tests-no-zope]", "zope.interface"]
-tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[docs,tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
 
 
 [[package]]
 [[package]]
 name = "autopep8"
 name = "autopep8"
@@ -488,25 +491,25 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
 
 
 [[package]]
 [[package]]
 name = "fastapi"
 name = "fastapi"
-version = "0.92.0"
+version = "0.95.1"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "fastapi-0.92.0-py3-none-any.whl", hash = "sha256:ae7b97c778e2f2ec3fb3cb4fb14162129411d99907fb71920f6d69a524340ebf"},
-    {file = "fastapi-0.92.0.tar.gz", hash = "sha256:023a0f5bd2c8b2609014d3bba1e14a1d7df96c6abea0a73070621c9862b9a4de"},
+    {file = "fastapi-0.95.1-py3-none-any.whl", hash = "sha256:a870d443e5405982e1667dfe372663abf10754f246866056336d7f01c21dab07"},
+    {file = "fastapi-0.95.1.tar.gz", hash = "sha256:9569f0a381f8a457ec479d90fa01005cfddaae07546eb1f3fa035bc4797ae7d5"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
 pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
 pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0"
-starlette = ">=0.25.0,<0.26.0"
+starlette = ">=0.26.1,<0.27.0"
 
 
 [package.extras]
 [package.extras]
 all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
 all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
 dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"]
 dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (>=0.12.0,<0.21.0)"]
-doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.8.0)"]
-test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.10.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.6.0.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
+doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"]
+test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"]
 
 
 [[package]]
 [[package]]
 name = "fastapi-socketio"
 name = "fastapi-socketio"
@@ -680,14 +683,14 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "importlib-metadata"
 name = "importlib-metadata"
-version = "6.3.0"
+version = "6.5.0"
 description = "Read metadata from Python packages"
 description = "Read metadata from Python packages"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "importlib_metadata-6.3.0-py3-none-any.whl", hash = "sha256:8f8bd2af397cf33bd344d35cfe7f489219b7d14fc79a3f854b75b8417e9226b0"},
-    {file = "importlib_metadata-6.3.0.tar.gz", hash = "sha256:23c2bcae4762dfb0bbe072d358faec24957901d75b6c4ab11172c0c982532402"},
+    {file = "importlib_metadata-6.5.0-py3-none-any.whl", hash = "sha256:03ba783c3a2c69d751b109fc0c94a62c51f581b3d6acf8ed1331b6d5729321ff"},
+    {file = "importlib_metadata-6.5.0.tar.gz", hash = "sha256:7a8bdf1bc3a726297f5cfbc999e6e7ff6b4fa41b26bba4afc580448624460045"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -1542,14 +1545,14 @@ email = ["email-validator (>=1.0.3)"]
 
 
 [[package]]
 [[package]]
 name = "pygments"
 name = "pygments"
-version = "2.15.0"
+version = "2.15.1"
 description = "Pygments is a syntax highlighting package written in Python."
 description = "Pygments is a syntax highlighting package written in Python."
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "Pygments-2.15.0-py3-none-any.whl", hash = "sha256:77a3299119af881904cd5ecd1ac6a66214b6e9bed1f2db16993b54adede64094"},
-    {file = "Pygments-2.15.0.tar.gz", hash = "sha256:f7e36cffc4c517fbc252861b9a6e4644ca0e5abadf9a113c72d1358ad09b9500"},
+    {file = "Pygments-2.15.1-py3-none-any.whl", hash = "sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1"},
+    {file = "Pygments-2.15.1.tar.gz", hash = "sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c"},
 ]
 ]
 
 
 [package.extras]
 [package.extras]
@@ -1557,58 +1560,58 @@ plugins = ["importlib-metadata"]
 
 
 [[package]]
 [[package]]
 name = "pyobjc-core"
 name = "pyobjc-core"
-version = "9.0.1"
+version = "9.1.1"
 description = "Python<->ObjC Interoperability Module"
 description = "Python<->ObjC Interoperability Module"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "pyobjc-core-9.0.1.tar.gz", hash = "sha256:5ce1510bb0bdff527c597079a42b2e13a19b7592e76850be7960a2775b59c929"},
-    {file = "pyobjc_core-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b614406d46175b1438a9596b664bf61952323116704d19bc1dea68052a0aad98"},
-    {file = "pyobjc_core-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bd397e729f6271c694fb70df8f5d3d3c9b2f2b8ac02fbbdd1757ca96027b94bb"},
-    {file = "pyobjc_core-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d919934eaa6d1cf1505ff447a5c2312be4c5651efcb694eb9f59e86f5bd25e6b"},
-    {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:67d67ca8b164f38ceacce28a18025845c3ec69613f3301935d4d2c4ceb22e3fd"},
-    {file = "pyobjc_core-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:39d11d71f6161ac0bd93cffc8ea210bb0178b56d16a7408bf74283d6ecfa7430"},
-    {file = "pyobjc_core-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25be1c4d530e473ed98b15063b8d6844f0733c98914de6f09fe1f7652b772bbc"},
+    {file = "pyobjc-core-9.1.1.tar.gz", hash = "sha256:4b6cb9053b5fcd3c0e76b8c8105a8110786b20f3403c5643a688c5ec51c55c6b"},
+    {file = "pyobjc_core-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4bd07049fd9fe5b40e4b7c468af9cf942508387faf383a5acb043d20627bad2c"},
+    {file = "pyobjc_core-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1a8307527621729ff2ab67860e7ed84f76ad0da881b248c2ef31e0da0088e4ba"},
+    {file = "pyobjc_core-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:083004d28b92ccb483a41195c600728854843b0486566aba2d6e63eef51f80e6"},
+    {file = "pyobjc_core-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d61e9517d451bc062a7fae8b3648f4deba4fa54a24926fa1cf581b90ef4ced5a"},
+    {file = "pyobjc_core-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1626909916603a3b04c07c721cf1af0e0b892cec85bb3db98d05ba024f1786fc"},
+    {file = "pyobjc_core-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2dde96462b52e952515d142e2afbb6913624a02c13582047e06211e6c3993728"},
 ]
 ]
 
 
 [[package]]
 [[package]]
 name = "pyobjc-framework-cocoa"
 name = "pyobjc-framework-cocoa"
-version = "9.0.1"
+version = "9.1.1"
 description = "Wrappers for the Cocoa frameworks on macOS"
 description = "Wrappers for the Cocoa frameworks on macOS"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "pyobjc-framework-Cocoa-9.0.1.tar.gz", hash = "sha256:a8b53b3426f94307a58e2f8214dc1094c19afa9dcb96f21be12f937d968b2df3"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f94b0f92a62b781e633e58f09bcaded63d612f9b1e15202f5f372ea59e4aebd"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f062c3bb5cc89902e6d164aa9a66ffc03638645dd5f0468b6f525ac997c86e51"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0b374c0a9d32ba4fc5610ab2741cb05a005f1dfb82a47dbf2dbb2b3a34b73ce5"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8928080cebbce91ac139e460d3dfc94c7cb6935be032dcae9c0a51b247f9c2d9"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:9d2bd86a0a98d906f762f5dc59f2fc67cce32ae9633b02ff59ac8c8a33dd862d"},
-    {file = "pyobjc_framework_Cocoa-9.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2a41053cbcee30e1e8914efa749c50b70bf782527d5938f2bc2a6393740969ce"},
+    {file = "pyobjc-framework-Cocoa-9.1.1.tar.gz", hash = "sha256:345c32b6d1f3db45f635e400f2d0d6c0f0f7349d45ec823f76fc1df43d13caeb"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9176a4276f3b4b4758e9b9ca10698be5341ceffaeaa4fa055133417179e6bc37"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5e1e96fb3461f46ff951413515f2029e21be268b0e033db6abee7b64ec8e93d3"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:083b195c496d30c6b9dd86126a6093c4b95e0138e9b052b13e54103fcc0b4872"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a1b3333b1aa045608848bd68bbab4c31171f36aeeaa2fabeb4527c6f6f1e33cd"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:54c017354671f0d955432986c42218e452ca69906a101c8e7acde8510432303a"},
+    {file = "pyobjc_framework_Cocoa-9.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:10c0075688ce95b92caf59e368585fffdcc98c919bc345067af070222f5d01d2"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
-pyobjc-core = ">=9.0.1"
+pyobjc-core = ">=9.1.1"
 
 
 [[package]]
 [[package]]
 name = "pyobjc-framework-webkit"
 name = "pyobjc-framework-webkit"
-version = "9.0.1"
+version = "9.1.1"
 description = "Wrappers for the framework WebKit on macOS"
 description = "Wrappers for the framework WebKit on macOS"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "pyobjc-framework-WebKit-9.0.1.tar.gz", hash = "sha256:82ed0cb273012b48f7489072d6e00579f42d54bc4543471c262db3e5c4bb9e87"},
-    {file = "pyobjc_framework_WebKit-9.0.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:037082f72fa1f1d87889fdc172726c3381769de24ca5207d596f3925df9b25f0"},
-    {file = "pyobjc_framework_WebKit-9.0.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:952685b820545036833ed737600d32c344916a83b2af4e04acb4b618aaac9431"},
-    {file = "pyobjc_framework_WebKit-9.0.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:28a7859401b5af7c47e17612b4b3baca6669e76f974f6f6bfe5e93921a00adec"},
+    {file = "pyobjc-framework-WebKit-9.1.1.tar.gz", hash = "sha256:bc6ba0ca6ed9ebcb4c5fc338410a81f50b4da08e4bab4bfe5853612e8e5e79aa"},
+    {file = "pyobjc_framework_WebKit-9.1.1-cp36-abi3-macosx_10_9_universal2.whl", hash = "sha256:49ffae7c9dcab8048601166c97e6352999b5cd837a73ae6c1d5a8b90aa892b6d"},
+    {file = "pyobjc_framework_WebKit-9.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1d371fb72aa06a3c3f3cbb1360e8762b225c2f62c28562218535ce17206163d4"},
+    {file = "pyobjc_framework_WebKit-9.1.1-cp36-abi3-macosx_11_0_universal2.whl", hash = "sha256:be51de0d13a1308cc67d3ac49eb30dffdf4c3a0b386e5515dfdab8c6b5fec4d4"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
-pyobjc-core = ">=9.0.1"
-pyobjc-framework-Cocoa = ">=9.0.1"
+pyobjc-core = ">=9.1.1"
+pyobjc-framework-Cocoa = ">=9.1.1"
 
 
 [[package]]
 [[package]]
 name = "pyparsing"
 name = "pyparsing"
@@ -1807,14 +1810,14 @@ cli = ["click (>=5.0)"]
 
 
 [[package]]
 [[package]]
 name = "python-engineio"
 name = "python-engineio"
-version = "4.4.0"
+version = "4.4.1"
 description = "Engine.IO server and client for Python"
 description = "Engine.IO server and client for Python"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.6"
 python-versions = ">=3.6"
 files = [
 files = [
-    {file = "python-engineio-4.4.0.tar.gz", hash = "sha256:bcc035c70ecc30acc3cfd49ef19aca6c51fa6caaadd0fa58c2d7480f50d04cf2"},
-    {file = "python_engineio-4.4.0-py3-none-any.whl", hash = "sha256:11f9c35b775fe70e0a25f67b16d5b69fbfafc368cdd87eeb6f4135a475c88e50"},
+    {file = "python-engineio-4.4.1.tar.gz", hash = "sha256:eb3663ecb300195926b526386f712dff84cd092c818fb7b62eeeda9160120c29"},
+    {file = "python_engineio-4.4.1-py3-none-any.whl", hash = "sha256:28ab67f94cba2e5f598cbb04428138fd6bb8b06d3478c939412da445f24f0773"},
 ]
 ]
 
 
 [package.extras]
 [package.extras]
@@ -2012,14 +2015,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 
 [[package]]
 [[package]]
 name = "selenium"
 name = "selenium"
-version = "4.8.3"
+version = "4.9.0"
 description = ""
 description = ""
 category = "dev"
 category = "dev"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "selenium-4.8.3-py3-none-any.whl", hash = "sha256:28430ac54a54fa59ad1f5392a1b89b169fe3ab2c2ccd1a9a10b6fe74f36cd6da"},
-    {file = "selenium-4.8.3.tar.gz", hash = "sha256:61cda3a304f82637162bc155cae7bf88fdb04c115fa2cb1c1c2e1358fcd19a9f"},
+    {file = "selenium-4.9.0-py3-none-any.whl", hash = "sha256:4c19e6aac202719373108d53a5a8e9336ba8d2b25822ca32ae6ff37acbabbdbe"},
+    {file = "selenium-4.9.0.tar.gz", hash = "sha256:478fae77cdfaec32adb1e68d59632c8c191f920535282abcaa2d1a3d98655624"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -2066,14 +2069,14 @@ files = [
 
 
 [[package]]
 [[package]]
 name = "starlette"
 name = "starlette"
-version = "0.25.0"
+version = "0.26.1"
 description = "The little ASGI library that shines."
 description = "The little ASGI library that shines."
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "starlette-0.25.0-py3-none-any.whl", hash = "sha256:774f1df1983fd594b9b6fb3ded39c2aa1979d10ac45caac0f4255cbe2acb8628"},
-    {file = "starlette-0.25.0.tar.gz", hash = "sha256:854c71e73736c429c2bdb07801f2c76c9cba497e7c3cf4988fde5e95fe4cdb3c"},
+    {file = "starlette-0.26.1-py3-none-any.whl", hash = "sha256:e87fce5d7cbdde34b76f0ac69013fd9d190d581d80681493016666e6f96c6d5e"},
+    {file = "starlette-0.26.1.tar.gz", hash = "sha256:41da799057ea8620e4667a3e69a5b1923ebd32b1819c8fa75634bbe8d8bea9bd"},
 ]
 ]
 
 
 [package.dependencies]
 [package.dependencies]
@@ -2316,82 +2319,82 @@ anyio = ">=3.0.0"
 
 
 [[package]]
 [[package]]
 name = "websockets"
 name = "websockets"
-version = "11.0.1"
+version = "11.0.2"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
 category = "main"
 category = "main"
 optional = false
 optional = false
 python-versions = ">=3.7"
 python-versions = ">=3.7"
 files = [
 files = [
-    {file = "websockets-11.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d30cc1a90bcbf9e22e1f667c1c5a7428e2d37362288b4ebfd5118eb0b11afa9"},
-    {file = "websockets-11.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dc77283a7c7b2b24e00fe8c3c4f7cf36bba4f65125777e906aae4d58d06d0460"},
-    {file = "websockets-11.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0929c2ebdf00cedda77bf77685693e38c269011236e7c62182fee5848c29a4fa"},
-    {file = "websockets-11.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db234da3aff01e8483cf0015b75486c04d50dbf90004bd3e5b46d384e1bd6c9e"},
-    {file = "websockets-11.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7fdfbed727ce6b4b5e6622d15a6efb2098b2d9e22ba4dc54b2e3ce80f982045"},
-    {file = "websockets-11.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5f3d0d177b3db3d1d02cce7ba6c0063586499ac28afe0c992be74ffc40d9257"},
-    {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:25ea5dbd3b00c56b034639dc6fe4d1dd095b8205bab1782d9a47cb020695fdf4"},
-    {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:dbeada3b8f1f6d9497840f761906c4236f912a42da4515520168bc7c525b52b0"},
-    {file = "websockets-11.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:892959b627eedcdf98ac7022f9f71f050a59624b380b67862da10c32ea3c221a"},
-    {file = "websockets-11.0.1-cp310-cp310-win32.whl", hash = "sha256:fc0a96a6828bfa6f1ccec62b54630bcdcc205d483f5a8806c0a8abb26101c54b"},
-    {file = "websockets-11.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:3a88375b648a2c479532943cc19a018df1e5fcea85d5f31963c0b22794d1bdc1"},
-    {file = "websockets-11.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3cf18bbd44b36749b7b66f047a30a40b799b8c0bd9a1b9173cba86a234b4306b"},
-    {file = "websockets-11.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:deb0dd98ea4e76b833f0bfd7a6042b51115360d5dfcc7c1daa72dfc417b3327a"},
-    {file = "websockets-11.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:45a85dc6b3ff76239379feb4355aadebc18d6e587c8deb866d11060755f4d3ea"},
-    {file = "websockets-11.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d68bd2a3e9fff6f7043c0a711cb1ebba9f202c196a3943d0c885650cd0b6464"},
-    {file = "websockets-11.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfd0b9b18d64c51e5cd322e16b5bf4fe490db65c9f7b18fd5382c824062ead7e"},
-    {file = "websockets-11.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef0e6253c36e42f2637cfa3ff9b3903df60d05ec040c718999f6a0644ce1c497"},
-    {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:12180bc1d72c6a9247472c1dee9dfd7fc2e23786f25feee7204406972d8dab39"},
-    {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:a797da96d4127e517a5cb0965cd03fd6ec21e02667c1258fa0579501537fbe5c"},
-    {file = "websockets-11.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:07cc20655fb16aeef1a8f03236ba8671c61d332580b996b6396a5b7967ba4b3d"},
-    {file = "websockets-11.0.1-cp311-cp311-win32.whl", hash = "sha256:a01c674e0efe0f14aec7e722ed0e0e272fa2f10e8ea8260837e1f4f5dc4b3e53"},
-    {file = "websockets-11.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2796f097841619acf053245f266a4f66cb27c040f0d9097e5f21301aab95ff43"},
-    {file = "websockets-11.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:54d084756c50dfc8086dce97b945f210ca43950154e1e04a44a30c6e6a2bcbb1"},
-    {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe2aed5963ca267c40a2d29b1ee4e8ab008ac8d5daa284fdda9275201b8a334"},
-    {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e92dbac318a84fef722f38ca57acef19cbb89527aba5d420b96aa2656970ee"},
-    {file = "websockets-11.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec4e87eb9916b481216b1fede7d8913be799915f5216a0c801867cbed8eeb903"},
-    {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d4e0990b6a04b07095c969969da659eecf9069cf8e7b8f49c8f5ee1bb50e3352"},
-    {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c90343fd0774749d23c1891dd8b3e9210f9afd30986673ce0f9d5857f5cb1562"},
-    {file = "websockets-11.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ac042e8ba9d7f2618e84af27927fdce0f3e03528eb74f343977486c093868389"},
-    {file = "websockets-11.0.1-cp37-cp37m-win32.whl", hash = "sha256:385c5391becb9b58e0a4f33345e12762fd857ccf9fbf6fee428669929ba45e4c"},
-    {file = "websockets-11.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:aef1602db81096ce3d3847865128c8879635bdad7963fb2b7df290edb9e9150a"},
-    {file = "websockets-11.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:52ba83ea132390e426f9a7b48848248a2dc0e7120ca8c65d5a8fc1efaa4eb51b"},
-    {file = "websockets-11.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:007ed0d62f7e06eeb6e3a848b0d83b9fbd9e14674a59a61326845f27d20d7452"},
-    {file = "websockets-11.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f888b9565ca1d1c25ab827d184f57f4772ffbfa6baf5710b873b01936cc335ee"},
-    {file = "websockets-11.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db78535b791840a584c48cf3f4215eae38a7e2f43271ecd27ce4ba8a798beaaa"},
-    {file = "websockets-11.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa1c23ed3a02732fba906ec337df65d4cc23f9f453635e1a803c285b59c7d987"},
-    {file = "websockets-11.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e039f106d48d3c241f1943bccfb383bd38ec39900d6dcaad0c73cc5fe129f346"},
-    {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b0ed24a3aa4213029e100257e5e73c5f912e70ca35630081de94b7f9e2cf4a9b"},
-    {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5a3022f9291bf2d35ebf65929297d625e68effd3a5647b8eb8b89d51b09394c"},
-    {file = "websockets-11.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1cb23597819f68ac6a6d133a002a1b3ef12a22850236b083242c93f81f206d5a"},
-    {file = "websockets-11.0.1-cp38-cp38-win32.whl", hash = "sha256:349dd1fa56a30d530555988be98013688de67809f384671883f8bf8b8c9de984"},
-    {file = "websockets-11.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:87ae582cf2319e45bc457a57232daded27a3c771263cab42fb8864214bbd74ea"},
-    {file = "websockets-11.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a88815a0c6253ad1312ef186620832fb347706c177730efec34e3efe75e0e248"},
-    {file = "websockets-11.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d5a6fa353b5ef36970c3bd1cd7cecbc08bb8f2f1a3d008b0691208cf34ebf5b0"},
-    {file = "websockets-11.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ffe6fc5e5fe9f2634cdc59b805e4ba1fcccf3a5622f5f36c3c7c287f606e283"},
-    {file = "websockets-11.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b138f4bf8a64c344e12c76283dac279d11adab89ac62ae4a32ac8490d3c94832"},
-    {file = "websockets-11.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aedd94422745da60672a901f53de1f50b16e85408b18672b9b210db4a776b5a6"},
-    {file = "websockets-11.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4667d4e41fa37fa3d836b2603b8b40d6887fa4838496d48791036394f7ace39"},
-    {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:43e0de552be624e5c0323ff4fcc9f0b4a9a6dc6e0116b8aa2cbb6e0d3d2baf09"},
-    {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ceeef57b9aec8f27e523de4da73c518ece7721aefe7064f18aa28baabfe61b94"},
-    {file = "websockets-11.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9d91279d57f6546eaf43671d1de50621e0578f13c2f17c96c458a72d170698d7"},
-    {file = "websockets-11.0.1-cp39-cp39-win32.whl", hash = "sha256:29282631da3bfeb5db497e4d3d94d56ee36222fbebd0b51014e68a2e70736fb1"},
-    {file = "websockets-11.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e2654e94c705ce9b768441d8e3a387a84951ca1056efdc4a26a4a6ee723c01b6"},
-    {file = "websockets-11.0.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:60a19d4ff5f451254f8623f6aa4169065f73a50ec7b59ab6b9dcddff4aa00267"},
-    {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a58e83f82098d062ae5d4cbe7073b8783999c284d6f079f2fefe87cd8957ac8"},
-    {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b91657b65355954e47f0df874917fa200426b3a7f4e68073326a8cfc2f6deef8"},
-    {file = "websockets-11.0.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53b8e1ee01eb5b8be5c8a69ae26b0820dbc198d092ad50b3451adc3cdd55d455"},
-    {file = "websockets-11.0.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:5d8d5d17371ed9eb9f0e3a8d326bdf8172700164c2e705bc7f1905a719a189be"},
-    {file = "websockets-11.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e53419201c6c1439148feb99de6b307651a88b8defd41348cc23bbe2a290de1d"},
-    {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718d19c494637f28e651031b3df6a791b9e86e0097c65ed5e8ec49b400b1210e"},
-    {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7b2544eb3e7bc39ce59812371214cd97762080dab90c3afc857890039384753"},
-    {file = "websockets-11.0.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec4a887d2236e3878c07033ad5566f6b4d5d954b85f92a219519a1745d0c93e9"},
-    {file = "websockets-11.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ae59a9f0a77ecb0cbdedea7d206a547ff136e8bfbc7d2d98772fb02d398797bb"},
-    {file = "websockets-11.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ef35cef161f76031f833146f895e7e302196e01c704c00d269c04d8e18f3ac37"},
-    {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79b6548e57ab18f071b9bfe3ffe02af7184dd899bc674e2817d8fe7e9e7489ec"},
-    {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8d9793f3fb0da16232503df14411dabafed5a81fc9077dc430cfc6f60e71179"},
-    {file = "websockets-11.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42aa05e890fcf1faed8e535c088a1f0f27675827cbacf62d3024eb1e6d4c9e0c"},
-    {file = "websockets-11.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5d4f4b341100d313b08149d7031eb6d12738ac758b0c90d2f9be8675f401b019"},
-    {file = "websockets-11.0.1-py3-none-any.whl", hash = "sha256:85b4127f7da332feb932eee833c70e5e1670469e8c9de7ef3874aa2a91a6fbb2"},
-    {file = "websockets-11.0.1.tar.gz", hash = "sha256:369410925b240b30ef1c1deadbd6331e9cd865ad0b8966bf31e276cc8e0da159"},
+    {file = "websockets-11.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:580cc95c58118f8c39106be71e24d0b7e1ad11a155f40a2ee687f99b3e5e432e"},
+    {file = "websockets-11.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:143782041e95b63083b02107f31cda999f392903ae331de1307441f3a4557d51"},
+    {file = "websockets-11.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8df63dcd955eb6b2e371d95aacf8b7c535e482192cff1b6ce927d8f43fb4f552"},
+    {file = "websockets-11.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9b2dced5cbbc5094678cc1ec62160f7b0fe4defd601cd28a36fde7ee71bbb5"},
+    {file = "websockets-11.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0eeeea3b01c97fd3b5049a46c908823f68b59bf0e18d79b231d8d6764bc81ee"},
+    {file = "websockets-11.0.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:502683c5dedfc94b9f0f6790efb26aa0591526e8403ad443dce922cd6c0ec83b"},
+    {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3cc3e48b6c9f7df8c3798004b9c4b92abca09eeea5e1b0a39698f05b7a33b9d"},
+    {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:808b8a33c961bbd6d33c55908f7c137569b09ea7dd024bce969969aa04ecf07c"},
+    {file = "websockets-11.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:34a6f8996964ccaa40da42ee36aa1572adcb1e213665e24aa2f1037da6080909"},
+    {file = "websockets-11.0.2-cp310-cp310-win32.whl", hash = "sha256:8f24cd758cbe1607a91b720537685b64e4d39415649cac9177cd1257317cf30c"},
+    {file = "websockets-11.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:3b87cd302f08ea9e74fdc080470eddbed1e165113c1823fb3ee6328bc40ca1d3"},
+    {file = "websockets-11.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3565a8f8c7bdde7c29ebe46146bd191290413ee6f8e94cf350609720c075b0a1"},
+    {file = "websockets-11.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f97e03d4d5a4f0dca739ea274be9092822f7430b77d25aa02da6775e490f6846"},
+    {file = "websockets-11.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8f392587eb2767afa8a34e909f2fec779f90b630622adc95d8b5e26ea8823cb8"},
+    {file = "websockets-11.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7742cd4524622cc7aa71734b51294644492a961243c4fe67874971c4d3045982"},
+    {file = "websockets-11.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46dda4bc2030c335abe192b94e98686615f9274f6b56f32f2dd661fb303d9d12"},
+    {file = "websockets-11.0.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6b2bfa1d884c254b841b0ff79373b6b80779088df6704f034858e4d705a4802"},
+    {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1df2413266bf48430ef2a752c49b93086c6bf192d708e4a9920544c74cd2baa6"},
+    {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf45d273202b0c1cec0f03a7972c655b93611f2e996669667414557230a87b88"},
+    {file = "websockets-11.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a09cce3dacb6ad638fdfa3154d9e54a98efe7c8f68f000e55ca9c716496ca67"},
+    {file = "websockets-11.0.2-cp311-cp311-win32.whl", hash = "sha256:2174a75d579d811279855df5824676d851a69f52852edb0e7551e0eeac6f59a4"},
+    {file = "websockets-11.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:c78ca3037a954a4209b9f900e0eabbc471fb4ebe96914016281df2c974a93e3e"},
+    {file = "websockets-11.0.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2100b02d1aaf66dc48ff1b2a72f34f6ebc575a02bc0350cc8e9fbb35940166"},
+    {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dca9708eea9f9ed300394d4775beb2667288e998eb6f542cdb6c02027430c599"},
+    {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:320ddceefd2364d4afe6576195201a3632a6f2e6d207b0c01333e965b22dbc84"},
+    {file = "websockets-11.0.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2a573c8d71b7af937852b61e7ccb37151d719974146b5dc734aad350ef55a02"},
+    {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:13bd5bebcd16a4b5e403061b8b9dcc5c77e7a71e3c57e072d8dff23e33f70fba"},
+    {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:95c09427c1c57206fe04277bf871b396476d5a8857fa1b99703283ee497c7a5d"},
+    {file = "websockets-11.0.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2eb042734e710d39e9bc58deab23a65bd2750e161436101488f8af92f183c239"},
+    {file = "websockets-11.0.2-cp37-cp37m-win32.whl", hash = "sha256:5875f623a10b9ba154cb61967f940ab469039f0b5e61c80dd153a65f024d9fb7"},
+    {file = "websockets-11.0.2-cp37-cp37m-win_amd64.whl", hash = "sha256:634239bc844131863762865b75211a913c536817c0da27f691400d49d256df1d"},
+    {file = "websockets-11.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3178d965ec204773ab67985a09f5696ca6c3869afeed0bb51703ea404a24e975"},
+    {file = "websockets-11.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:955fcdb304833df2e172ce2492b7b47b4aab5dcc035a10e093d911a1916f2c87"},
+    {file = "websockets-11.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cb46d2c7631b2e6f10f7c8bac7854f7c5e5288f024f1c137d4633c79ead1e3c0"},
+    {file = "websockets-11.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25aae96c1060e85836552a113495db6d857400288161299d77b7b20f2ac569f2"},
+    {file = "websockets-11.0.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2abeeae63154b7f63d9f764685b2d299e9141171b8b896688bd8baec6b3e2303"},
+    {file = "websockets-11.0.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:daa1e8ea47507555ed7a34f8b49398d33dff5b8548eae3de1dc0ef0607273a33"},
+    {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:954eb789c960fa5daaed3cfe336abc066941a5d456ff6be8f0e03dd89886bb4c"},
+    {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3ffe251a31f37e65b9b9aca5d2d67fd091c234e530f13d9dce4a67959d5a3fba"},
+    {file = "websockets-11.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:adf6385f677ed2e0b021845b36f55c43f171dab3a9ee0ace94da67302f1bc364"},
+    {file = "websockets-11.0.2-cp38-cp38-win32.whl", hash = "sha256:aa7b33c1fb2f7b7b9820f93a5d61ffd47f5a91711bc5fa4583bbe0c0601ec0b2"},
+    {file = "websockets-11.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:220d5b93764dd70d7617f1663da64256df7e7ea31fc66bc52c0e3750ee134ae3"},
+    {file = "websockets-11.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0fb4480556825e4e6bf2eebdbeb130d9474c62705100c90e59f2f56459ddab42"},
+    {file = "websockets-11.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ec00401846569aaf018700249996143f567d50050c5b7b650148989f956547af"},
+    {file = "websockets-11.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87c69f50281126dcdaccd64d951fb57fbce272578d24efc59bce72cf264725d0"},
+    {file = "websockets-11.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:232b6ba974f5d09b1b747ac232f3a3d8f86de401d7b565e837cc86988edf37ac"},
+    {file = "websockets-11.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:392d409178db1e46d1055e51cc850136d302434e12d412a555e5291ab810f622"},
+    {file = "websockets-11.0.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4fe2442091ff71dee0769a10449420fd5d3b606c590f78dd2b97d94b7455640"},
+    {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ede13a6998ba2568b21825809d96e69a38dc43184bdeebbde3699c8baa21d015"},
+    {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4c54086b2d2aec3c3cb887ad97e9c02c6be9f1d48381c7419a4aa932d31661e4"},
+    {file = "websockets-11.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e37a76ccd483a6457580077d43bc3dfe1fd784ecb2151fcb9d1c73f424deaeba"},
+    {file = "websockets-11.0.2-cp39-cp39-win32.whl", hash = "sha256:d1881518b488a920434a271a6e8a5c9481a67c4f6352ebbdd249b789c0467ddc"},
+    {file = "websockets-11.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:25e265686ea385f22a00cc2b719b880797cd1bb53b46dbde969e554fb458bfde"},
+    {file = "websockets-11.0.2-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ce69f5c742eefd039dce8622e99d811ef2135b69d10f9aa79fbf2fdcc1e56cd7"},
+    {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b985ba2b9e972cf99ddffc07df1a314b893095f62c75bc7c5354a9c4647c6503"},
+    {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b52def56d2a26e0e9c464f90cadb7e628e04f67b0ff3a76a4d9a18dfc35e3dd"},
+    {file = "websockets-11.0.2-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d70a438ef2a22a581d65ad7648e949d4ccd20e3c8ed7a90bbc46df4e60320891"},
+    {file = "websockets-11.0.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:752fbf420c71416fb1472fec1b4cb8631c1aa2be7149e0a5ba7e5771d75d2bb9"},
+    {file = "websockets-11.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:dd906b0cdc417ea7a5f13bb3c6ca3b5fd563338dc596996cb0fdd7872d691c0a"},
+    {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e79065ff6549dd3c765e7916067e12a9c91df2affea0ac51bcd302aaf7ad207"},
+    {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:46388a050d9e40316e58a3f0838c63caacb72f94129eb621a659a6e49bad27ce"},
+    {file = "websockets-11.0.2-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c7de298371d913824f71b30f7685bb07ad13969c79679cca5b1f7f94fec012f"},
+    {file = "websockets-11.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:6d872c972c87c393e6a49c1afbdc596432df8c06d0ff7cd05aa18e885e7cfb7c"},
+    {file = "websockets-11.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b444366b605d2885f0034dd889faf91b4b47668dd125591e2c64bfde611ac7e1"},
+    {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b967a4849db6b567dec3f7dd5d97b15ce653e3497b8ce0814e470d5e074750"},
+    {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2acdc82099999e44fa7bd8c886f03c70a22b1d53ae74252f389be30d64fd6004"},
+    {file = "websockets-11.0.2-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:518ed6782d9916c5721ebd61bb7651d244178b74399028302c8617d0620af291"},
+    {file = "websockets-11.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:58477b041099bb504e1a5ddd8aa86302ed1d5c6995bdd3db2b3084ef0135d277"},
+    {file = "websockets-11.0.2-py3-none-any.whl", hash = "sha256:5004c087d17251938a52cce21b3dbdabeecbbe432ce3f5bbbf15d8692c36eac9"},
+    {file = "websockets-11.0.2.tar.gz", hash = "sha256:b1a69701eb98ed83dd099de4a686dc892c413d974fa31602bc00aca7cb988ac9"},
 ]
 ]
 
 
 [[package]]
 [[package]]
@@ -2428,4 +2431,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 [metadata]
 lock-version = "2.0"
 lock-version = "2.0"
 python-versions = "^3.7"
 python-versions = "^3.7"
-content-hash = "5bf762587752502c0e0a137e7356b7a8a6eac24384f9e2fa0a8799328583e573"
+content-hash = "d77f95c9aec12c19ab35ade33d4e9f9acad27e48175f40a7f9197c6f771d3a91"

+ 1 - 1
pyproject.toml

@@ -18,7 +18,7 @@ matplotlib = [
     { version = "^3.5.0", markers = "python_version ~= '3.7'"},
     { version = "^3.5.0", markers = "python_version ~= '3.7'"},
     { version = "^3.6.0", markers = "python_version ~= '3.11.0'"},
     { version = "^3.6.0", markers = "python_version ~= '3.11.0'"},
 ]
 ]
-fastapi = "^0.92"
+fastapi = ">=0.92,<1.0.0"
 fastapi-socketio = "^0.0.10"
 fastapi-socketio = "^0.0.10"
 vbuild = "^0.8.1"
 vbuild = "^0.8.1"
 watchfiles = "^0.18.1"
 watchfiles = "^0.18.1"

+ 0 - 14
tests/input.py

@@ -1,14 +0,0 @@
-from nicegui import ui
-
-from .screen import Screen
-
-
-def test_input_with_multi_word_error_message(screen: Screen):
-    input = ui.input(label='some input')
-    ui.button('set error', on_click=lambda: input.props('error error-message="Some multi word error message"'))
-
-    screen.open('/')
-    screen.should_not_contain('Some multi word error message')
-
-    screen.click('set error')
-    screen.should_contain('Some multi word error message')

+ 17 - 0
tests/test_button.py

@@ -16,3 +16,20 @@ def test_quasar_colors(screen: Screen):
     assert screen.find_by_id(b3.id).value_of_css_property('background-color') == 'rgba(239, 83, 80, 1)'
     assert screen.find_by_id(b3.id).value_of_css_property('background-color') == 'rgba(239, 83, 80, 1)'
     assert screen.find_by_id(b4.id).value_of_css_property('background-color') == 'rgba(239, 68, 68, 1)'
     assert screen.find_by_id(b4.id).value_of_css_property('background-color') == 'rgba(239, 68, 68, 1)'
     assert screen.find_by_id(b5.id).value_of_css_property('background-color') == 'rgba(255, 0, 0, 1)'
     assert screen.find_by_id(b5.id).value_of_css_property('background-color') == 'rgba(255, 0, 0, 1)'
+
+
+def test_enable_disable(screen: Screen):
+    events = []
+    b = ui.button('Button', on_click=lambda: events.append(1))
+    ui.button('Enable', on_click=b.enable)
+    ui.button('Disable', on_click=b.disable)
+
+    screen.open('/')
+    screen.click('Button')
+    assert events == [1]
+    screen.click('Disable')
+    screen.click('Button')
+    assert events == [1]
+    screen.click('Enable')
+    screen.click('Button')
+    assert events == [1, 1]

+ 36 - 0
tests/test_dark_mode.py

@@ -0,0 +1,36 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_dark_mode(screen: Screen):
+    ui.label('Hello')
+    dark = ui.dark_mode()
+    ui.button('Dark', on_click=dark.enable)
+    ui.button('Light', on_click=dark.disable)
+    ui.button('Auto', on_click=dark.auto)
+    ui.button('Toggle', on_click=dark.toggle)
+
+    screen.open('/')
+    screen.should_contain('Hello')
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light'
+
+    screen.click('Dark')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--dark dark'
+
+    screen.click('Auto')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light'
+
+    screen.click('Toggle')
+    screen.wait(0.5)
+    screen.assert_py_logger('ERROR', 'Cannot toggle dark mode when it is set to auto.')
+
+    screen.click('Light')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--light'
+
+    screen.click('Toggle')
+    screen.wait(0.5)
+    assert screen.find_by_tag('body').get_attribute('class') == 'desktop no-touch body--dark dark'

+ 11 - 0
tests/test_input.py

@@ -66,3 +66,14 @@ def test_input_validation(screen: Screen):
     element.send_keys(' Doe')
     element.send_keys(' Doe')
     screen.wait(0.5)
     screen.wait(0.5)
     screen.should_not_contain('Too short')
     screen.should_not_contain('Too short')
+
+
+def test_input_with_multi_word_error_message(screen: Screen):
+    input = ui.input(label='some input')
+    ui.button('set error', on_click=lambda: input.props('error error-message="Some multi word error message"'))
+
+    screen.open('/')
+    screen.should_not_contain('Some multi word error message')
+
+    screen.click('set error')
+    screen.should_contain('Some multi word error message')

+ 7 - 0
tests/test_label.py

@@ -8,3 +8,10 @@ def test_hello_world(screen: Screen):
 
 
     screen.open('/')
     screen.open('/')
     screen.should_contain('Hello, world')
     screen.should_contain('Hello, world')
+
+
+def test_text_0(screen: Screen):
+    ui.label(0)
+
+    screen.open('/')
+    screen.should_contain('0')

+ 13 - 0
tests/test_number.py

@@ -16,3 +16,16 @@ def test_apply_format_on_blur(screen: Screen):
     element.send_keys('789')
     element.send_keys('789')
     screen.click('Button')
     screen.click('Button')
     screen.should_contain_input('3.1417')
     screen.should_contain_input('3.1417')
+
+
+def test_max_value(screen: Screen):
+    ui.number('Number', min=0, max=10, value=5)
+    ui.button('Button')
+
+    screen.open('/')
+    screen.should_contain_input('5')
+
+    element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Number"]')
+    element.send_keys('6')
+    screen.click('Button')
+    screen.should_contain_input('10')

+ 37 - 0
tests/test_upload.py

@@ -56,3 +56,40 @@ def test_uploading_from_two_tabs(screen: Screen):
     screen.should_contain(f'uploaded {test_path1.name}')
     screen.should_contain(f'uploaded {test_path1.name}')
     screen.switch_to(0)
     screen.switch_to(0)
     screen.should_not_contain(f'uploaded {test_path1.name}')
     screen.should_not_contain(f'uploaded {test_path1.name}')
+
+
+def test_upload_with_header_slot(screen: Screen):
+    with ui.upload().add_slot('header'):
+        ui.label('Header')
+
+    screen.open('/')
+    screen.should_contain('Header')
+
+
+def test_replace_upload(screen: Screen):
+    with ui.row() as container:
+        ui.upload(label='A')
+
+    def replace():
+        container.clear()
+        with container:
+            ui.upload(label='B')
+    ui.button('Replace', on_click=replace)
+
+    screen.open('/')
+    screen.should_contain('A')
+    screen.click('Replace')
+    screen.should_contain('B')
+    screen.should_not_contain('A')
+
+
+def test_reset_upload(screen: Screen):
+    upload = ui.upload()
+    ui.button('Reset', on_click=upload.reset)
+
+    screen.open('/')
+    screen.selenium.find_element(By.CLASS_NAME, 'q-uploader__input').send_keys(str(test_path1))
+    screen.should_contain(test_path1.name)
+    screen.click('Reset')
+    screen.wait(0.5)
+    screen.should_not_contain(test_path1.name)

+ 52 - 63
website/documentation.py

@@ -234,71 +234,59 @@ def create_full() -> None:
         ui.button().props('icon=touch_app outline round').classes('shadow-lg')
         ui.button().props('icon=touch_app outline round').classes('shadow-lg')
         ui.label('Stylish!').style('color: #6E93D6; font-size: 200%; font-weight: 300')
         ui.label('Stylish!').style('color: #6E93D6; font-size: 200%; font-weight: 300')
 
 
-    elements_list = ['ui.label', 'ui.checkbox', 'ui.switch', 'ui.input', 'ui.textarea', 'ui.button']
-
-    @ui.refreshable
-    def elements_ui():
-        with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
-            with demo.python_window(classes='w-full max-w-[44rem] grow'):
-                with ui.column().classes('w-full gap-4'):
-                    ui.markdown(f'''
-                    ```py
-                    from nicegui import ui
-
-                    element = {select_element.value}('element')
-                    ```
-                    ''').classes('mb-[-0.25em]')
-                    with ui.row().classes('items-center gap-0 w-full px-2'):
-                        def handle_classes(e: events.ValueChangeEventArguments):
-                            try:
-                                b.classes(replace=e.value)
-                            except ValueError:
-                                pass
-                        ui.markdown("`element.classes('`")
-                        ui.input(on_change=handle_classes).classes('mt-[-0.5em] text-mono grow').props('dense')
-                        ui.markdown("`')`")
-                    with ui.row().classes('items-center gap-0 w-full px-2'):
-                        def handle_props(e: events.ValueChangeEventArguments):
-                            b._props = {'label': 'Button', 'color': 'primary'}
-                            try:
-                                b.props(e.value)
-                            except ValueError:
-                                pass
-                            b.update()
-                        ui.markdown("`element.props('`")
-                        ui.input(on_change=handle_props).classes('mt-[-0.5em] text-mono grow').props('dense')
-                        ui.markdown("`')`")
-                    with ui.row().classes('items-center gap-0 w-full px-2'):
-                        def handle_style(e: events.ValueChangeEventArguments):
-                            try:
-                                b.style(replace=e.value)
-                            except ValueError:
-                                pass
-                        ui.markdown("`element.style('`")
-                        ui.input(on_change=handle_style).classes('mt-[-0.5em] text-mono grow').props('dense')
-                        ui.markdown("`')`")
-                    ui.markdown('''
-                    ```py
-                    ui.run()
-                    ```
-                    ''')
-            with demo.browser_window(classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
-                b = eval(f'{select_element.value}("element")')
-
     subheading('Try styling NiceGUI elements!')
     subheading('Try styling NiceGUI elements!')
-
     ui.markdown('''
     ui.markdown('''
-            Try out how
-            [Tailwind CSS classes](https://tailwindcss.com/),
-            [Quasar props](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components),
-            and CSS styles affect NiceGUI elements.
-        ''').classes('bold-links arrow-links')
-    with ui.row():
-        ui.label('Choose your favorite element from those available and start having fun!')
-        select_element = ui.select(options=elements_list, value='ui.button', on_change=elements_ui.refresh).props(
-            'borderless dense hide-bottom-space filled ')
-
-    elements_ui()
+        Try out how
+        [Tailwind CSS classes](https://tailwindcss.com/),
+        [Quasar props](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components),
+        and CSS styles affect NiceGUI elements.
+    ''').classes('bold-links arrow-links')
+    with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
+        with demo.python_window(classes='w-full max-w-[44rem]'):
+            with ui.column().classes('w-full gap-4'):
+                ui.markdown('''
+                ```py
+                from nicegui import ui
+
+                button = ui.button('Button')
+                ```
+                ''').classes('mb-[-0.25em]')
+                with ui.row().classes('items-center gap-0 w-full px-2'):
+                    def handle_classes(e: events.ValueChangeEventArguments):
+                        try:
+                            b.classes(replace=e.value)
+                        except ValueError:
+                            pass
+                    ui.markdown("`button.classes('`")
+                    ui.input(on_change=handle_classes).classes('mt-[-0.5em] text-mono grow').props('dense')
+                    ui.markdown("`')`")
+                with ui.row().classes('items-center gap-0 w-full px-2'):
+                    def handle_props(e: events.ValueChangeEventArguments):
+                        b._props = {'label': 'Button', 'color': 'primary'}
+                        try:
+                            b.props(e.value)
+                        except ValueError:
+                            pass
+                        b.update()
+                    ui.markdown("`button.props('`")
+                    ui.input(on_change=handle_props).classes('mt-[-0.5em] text-mono grow').props('dense')
+                    ui.markdown("`')`")
+                with ui.row().classes('items-center gap-0 w-full px-2'):
+                    def handle_style(e: events.ValueChangeEventArguments):
+                        try:
+                            b.style(replace=e.value)
+                        except ValueError:
+                            pass
+                    ui.markdown("`button.style('`")
+                    ui.input(on_change=handle_style).classes('mt-[-0.5em] text-mono grow').props('dense')
+                    ui.markdown("`')`")
+                ui.markdown('''
+                ```py
+                ui.run()
+                ```
+                ''')
+        with demo.browser_window(classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
+            b = ui.button('Button')
 
 
     @text_demo('Tailwind CSS', '''
     @text_demo('Tailwind CSS', '''
         [Tailwind CSS](https://tailwindcss.com/) is a CSS framework for rapidly building custom user interfaces.
         [Tailwind CSS](https://tailwindcss.com/) is a CSS framework for rapidly building custom user interfaces.
@@ -324,6 +312,7 @@ def create_full() -> None:
 
 
     load_demo(ui.query)
     load_demo(ui.query)
     load_demo(ui.colors)
     load_demo(ui.colors)
+    load_demo(ui.dark_mode)
 
 
     heading('Action')
     heading('Action')
 
 

+ 13 - 0
website/more_documentation/dark_mode_documentation.py

@@ -0,0 +1,13 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    dark = ui.dark_mode()
+    # ui.label('Switch mode:')
+    # ui.button('Dark', on_click=dark.enable)
+    # ui.button('Light', on_click=dark.disable)
+    # END OF DEMO
+    l = ui.label('Switch mode:')
+    c = l.parent_slot.parent
+    ui.button('Dark', on_click=lambda: (l.style('color: white'), c.style('background-color: var(--q-dark-page)')))
+    ui.button('Light', on_click=lambda: (l.style('color: default'), c.style('background-color: default')))

+ 10 - 0
website/more_documentation/input_documentation.py

@@ -1,8 +1,18 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from ..documentation_tools import text_demo
+
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     ui.input(label='Text', placeholder='start typing',
     ui.input(label='Text', placeholder='start typing',
              on_change=lambda e: result.set_text('you typed: ' + e.value),
              on_change=lambda e: result.set_text('you typed: ' + e.value),
              validation={'Input too long': lambda value: len(value) < 20})
              validation={'Input too long': lambda value: len(value) < 20})
     result = ui.label()
     result = ui.label()
+
+
+def more() -> None:
+
+    @text_demo('Auto complete input', ' The `autocomplete` feature provides suggestions as you type, making input easier and faster. The parameter `options` is a list of strings that contains the available options that will appear.')
+    async def autocompleteinput():
+        options = ['AutoComplete', 'NiceGUI', 'Awesome']
+        ui.input(label='Text', placeholder='start typing', autocomplete=options)

+ 10 - 0
website/more_documentation/slider_documentation.py

@@ -35,3 +35,13 @@ def more() -> None:
         ui.slider(min=0, max=10, step=0.1, value=5).props('label-always') \
         ui.slider(min=0, max=10, step=0.1, value=5).props('label-always') \
             .on('update:model-value', lambda msg: ui.notify(f'{msg["args"]}'),
             .on('update:model-value', lambda msg: ui.notify(f'{msg["args"]}'),
                 throttle=1.0, leading_events=False)
                 throttle=1.0, leading_events=False)
+
+    @text_demo('Disable slider', '''
+        You can disable a slider with the `disable()` method.
+        This will prevent the user from moving the slider.
+        The slider will also be grayed out.
+    ''')
+    def disable_slider():
+        slider = ui.slider(min=0, max=100, value=50)
+        ui.button('Disable slider', on_click=slider.disable)
+        ui.button('Enable slider', on_click=slider.enable)

+ 12 - 0
website/more_documentation/table_documentation.py

@@ -128,3 +128,15 @@ def more() -> None:
             </q-tr>
             </q-tr>
         ''')
         ''')
         table.on('rename', rename)
         table.on('rename', rename)
+
+    @text_demo('Table from pandas dataframe', '''
+        Here is a demo of how to create a table from a pandas dataframe.
+    ''')
+    def table_from_pandas_demo():
+        import pandas as pd
+
+        df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]})
+        ui.table(
+            columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
+            rows=df.to_dict('records'),
+        )

+ 21 - 12
website/more_documentation/timer_documentation.py

@@ -1,19 +1,28 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from ..documentation_tools import text_demo
+
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     from datetime import datetime
     from datetime import datetime
 
 
-    with ui.row().classes('items-center'):
-        clock = ui.label()
-        t = ui.timer(interval=0.1, callback=lambda: clock.set_text(datetime.now().strftime('%X.%f')[:-5]))
-        ui.checkbox('active').bind_value(t, 'active')
+    label = ui.label()
+    ui.timer(1.0, lambda: label.set_text(f'{datetime.now():%X}'))
+
+
+def more() -> None:
+    @text_demo('Activate and deactivate a timer', '''
+        You can activate and deactivate a timer using the `active` property.
+    ''')
+    def activate_deactivate_demo():
+        slider = ui.slider(min=-1, max=1, value=0)
+        timer = ui.timer(0.1, lambda: slider.set_value((slider.value + 0.01) % 1.0))
+        ui.switch('active').bind_value_to(timer, 'active')
 
 
-    with ui.row():
-        def lazy_update() -> None:
-            new_text = datetime.now().strftime('%X.%f')[:-5]
-            if lazy_clock.text[:8] == new_text[:8]:
-                return
-            lazy_clock.text = new_text
-        lazy_clock = ui.label()
-        ui.timer(interval=0.1, callback=lazy_update)
+    @text_demo('Call a function after a delay', '''
+        You can call a function after a delay using a timer with the `once` parameter.
+    ''')
+    def call_after_delay_demo():
+        def handle_click():
+            ui.timer(1.0, lambda: ui.notify('Hi!'), once=True)
+        ui.button('Notify after 1 second', on_click=handle_click)