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
   orcid: https://orcid.org/0009-0009-4735-6227
 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
-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
 
-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.
 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>"
 
-RUN pip install itsdangerous prometheus_client isort docutils
+RUN pip install itsdangerous prometheus_client isort docutils pandas
 
 WORKDIR /app
 

+ 1 - 1
fly.toml

@@ -12,7 +12,7 @@ processes = []
 [deploy]
 # 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
-strategy = "canary" 
+strategy = "rolling" 
 
 
 [experimental]

+ 1 - 1
nicegui/element.py

@@ -41,7 +41,7 @@ class Element(Visibility):
         self._style: Dict[str, str] = {}
         self._props: Dict[str, Any] = {}
         self._event_listeners: Dict[str, EventListener] = {}
-        self._text: str = ''
+        self._text: Optional[str] = None
         self.slots: Dict[str, Slot] = {}
         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 ..events import ClickEventArguments, handle_event
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 
 
-class Button(TextElement):
+class Button(TextElement, DisableableElement):
 
     def __init__(self,
                  text: str = '', *,

+ 2 - 1
nicegui/elements/checkbox.py

@@ -1,10 +1,11 @@
 from typing import Callable, Optional
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 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:
         """Checkbox

+ 2 - 1
nicegui/elements/color_input.py

@@ -3,10 +3,11 @@ from typing import Callable, Optional
 from nicegui import ui
 
 from .color_picker import ColorPicker
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class ColorInput(ValueElement):
+class ColorInput(ValueElement, DisableableElement):
     LOOPBACK = False
 
     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 .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class Date(ValueElement):
+class Date(ValueElement, DisableableElement):
     EVENT_ARGS = None
 
     def __init__(self,

+ 2 - 1
nicegui/elements/expansion.py

@@ -1,9 +1,10 @@
 from typing import Optional
 
+from .mixins.disableable_element import DisableableElement
 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:
         '''Expansion Element

+ 24 - 2
nicegui/elements/input.py

@@ -1,10 +1,11 @@
 from typing import Any, Callable, Dict, Optional
 
 from .icon import Icon
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class Input(ValueElement):
+class Input(ValueElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
@@ -14,6 +15,7 @@ class Input(ValueElement):
                  password: bool = False,
                  password_toggle_button: bool = False,
                  on_change: Optional[Callable] = None,
+                 autocomplete: Optional[list] = None,
                  validation: Dict[str, Callable] = {}) -> None:
         """Text Input
 
@@ -31,7 +33,8 @@ class Input(ValueElement):
         :param value: the current value of the text input
         :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 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}``
         """
         super().__init__(tag='q-input', value=value, on_value_change=on_change)
@@ -51,6 +54,25 @@ class Input(ValueElement):
 
         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:
         super().on_value_change(value)
         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 .label import Label
+from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class Knob(ValueElement):
+class Knob(ValueElement, DisableableElement):
 
     def __init__(self,
                  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 .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
 
 
-class Number(ValueElement):
+class Number(ValueElement, DisableableElement):
     LOOPBACK = False
 
     def __init__(self,
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = 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,
                  on_change: Optional[Callable] = None,
                  validation: Dict[str, Callable] = {}) -> None:
@@ -23,6 +29,11 @@ class Number(ValueElement):
         :param label: displayed name for the number input
         :param placeholder: text to show if no value is entered
         :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 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}``
@@ -34,8 +45,24 @@ class Number(ValueElement):
             self._props['label'] = label
         if placeholder is not None:
             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.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:
         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 .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):
         """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 .choice_element import ChoiceElement
+from .mixins.disableable_element import DisableableElement
 
 register_component('select', __file__, 'select.js')
 
 
-class Select(ChoiceElement):
+class Select(ChoiceElement, DisableableElement):
 
     def __init__(self, options: Union[List, Dict], *,
                  label: Optional[str] = None,

+ 2 - 1
nicegui/elements/slider.py

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

+ 3 - 1
nicegui/elements/splitter.py

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

+ 2 - 1
nicegui/elements/switch.py

@@ -1,10 +1,11 @@
 from typing import Callable, Optional
 
+from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
 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:
         """Switch

+ 5 - 5
nicegui/elements/tabs.py

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

+ 2 - 1
nicegui/elements/time.py

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

+ 2 - 1
nicegui/elements/toggle.py

@@ -1,9 +1,10 @@
 from typing import Any, Callable, Dict, List, Optional, Union
 
 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:
         """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 ..element import Element
+from ..dependencies import register_component
 from ..events import EventArguments, UploadEventArguments, handle_event
 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, *,
                  multiple: bool = False,
@@ -32,7 +35,7 @@ class Upload(Element):
         :param label: label for the uploader (default: `''`)
         :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['label'] = label
         self._props['auto-upload'] = auto_upload

+ 11 - 4
nicegui/functions/refreshable.py

@@ -1,5 +1,7 @@
 from typing import Callable, List
 
+from typing_extensions import Self
+
 from ..element import Element
 
 
@@ -8,19 +10,24 @@ class refreshable:
     def __init__(self, func: Callable) -> None:
         """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.
         """
         self.func = func
+        self.instance = None
         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:
-            self.func(*args, **kwargs)
+            self.func() if self.instance is None else self.func(self.instance)
         self.containers.append(container)
 
     def refresh(self) -> None:
         for container in self.containers:
             container.clear()
             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 multiprocessing
 import socket
+import sys
 import tempfile
 import time
 import warnings
@@ -8,10 +9,13 @@ from threading import Thread
 
 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:
@@ -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.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:

+ 6 - 10
nicegui/nicegui.py

@@ -8,6 +8,7 @@ from typing import Dict, Optional
 from fastapi import HTTPException, Request
 from fastapi.middleware.gzip import GZipMiddleware
 from fastapi.responses import FileResponse, Response
+from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
 from nicegui import json
@@ -28,6 +29,11 @@ socket_manager = SocketManager(app=app, mount_location='/_nicegui_ws/', json=jso
 globals.sio = sio = app.sio
 
 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__()
 
@@ -37,16 +43,6 @@ def index(request: Request) -> Response:
     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}')
 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:

+ 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 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 **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.title = title

+ 1 - 1
nicegui/templates/index.html

@@ -104,7 +104,7 @@
               }));
             }
             const children = data.ids.map(id => renderRecursively(elements, id));
-            if (name === 'default' && element.text) {
+            if (name === 'default' && element.text !== null) {
               children.unshift(element.text);
             }
             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.colors import Colors as colors
 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.dialog import Dialog as dialog
 from .elements.expansion import Expansion as expansion

+ 129 - 126
poetry.lock

@@ -65,22 +65,25 @@ files = [
 
 [[package]]
 name = "attrs"
-version = "22.2.0"
+version = "23.1.0"
 description = "Classes Without Boilerplate"
 category = "dev"
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.7"
 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]
-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]]
 name = "autopep8"
@@ -488,25 +491,25 @@ tests = ["asttokens", "littleutils", "pytest", "rich"]
 
 [[package]]
 name = "fastapi"
-version = "0.92.0"
+version = "0.95.1"
 description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
 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]
 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)"]
-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]]
 name = "fastapi-socketio"
@@ -680,14 +683,14 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "6.3.0"
+version = "6.5.0"
 description = "Read metadata from Python packages"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
@@ -1542,14 +1545,14 @@ email = ["email-validator (>=1.0.3)"]
 
 [[package]]
 name = "pygments"
-version = "2.15.0"
+version = "2.15.1"
 description = "Pygments is a syntax highlighting package written in Python."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
@@ -1557,58 +1560,58 @@ plugins = ["importlib-metadata"]
 
 [[package]]
 name = "pyobjc-core"
-version = "9.0.1"
+version = "9.1.1"
 description = "Python<->ObjC Interoperability Module"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]]
 name = "pyobjc-framework-cocoa"
-version = "9.0.1"
+version = "9.1.1"
 description = "Wrappers for the Cocoa frameworks on macOS"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
-pyobjc-core = ">=9.0.1"
+pyobjc-core = ">=9.1.1"
 
 [[package]]
 name = "pyobjc-framework-webkit"
-version = "9.0.1"
+version = "9.1.1"
 description = "Wrappers for the framework WebKit on macOS"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
-pyobjc-core = ">=9.0.1"
-pyobjc-framework-Cocoa = ">=9.0.1"
+pyobjc-core = ">=9.1.1"
+pyobjc-framework-Cocoa = ">=9.1.1"
 
 [[package]]
 name = "pyparsing"
@@ -1807,14 +1810,14 @@ cli = ["click (>=5.0)"]
 
 [[package]]
 name = "python-engineio"
-version = "4.4.0"
+version = "4.4.1"
 description = "Engine.IO server and client for Python"
 category = "main"
 optional = false
 python-versions = ">=3.6"
 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]
@@ -2012,14 +2015,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "selenium"
-version = "4.8.3"
+version = "4.9.0"
 description = ""
 category = "dev"
 optional = false
 python-versions = ">=3.7"
 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]
@@ -2066,14 +2069,14 @@ files = [
 
 [[package]]
 name = "starlette"
-version = "0.25.0"
+version = "0.26.1"
 description = "The little ASGI library that shines."
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]
@@ -2316,82 +2319,82 @@ anyio = ">=3.0.0"
 
 [[package]]
 name = "websockets"
-version = "11.0.1"
+version = "11.0.2"
 description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
 category = "main"
 optional = false
 python-versions = ">=3.7"
 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]]
@@ -2428,4 +2431,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more
 [metadata]
 lock-version = "2.0"
 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.6.0", markers = "python_version ~= '3.11.0'"},
 ]
-fastapi = "^0.92"
+fastapi = ">=0.92,<1.0.0"
 fastapi-socketio = "^0.0.10"
 vbuild = "^0.8.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(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)'
+
+
+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')
     screen.wait(0.5)
     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.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')
     screen.click('Button')
     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.switch_to(0)
     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.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!')
-
     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', '''
         [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.colors)
+    load_demo(ui.dark_mode)
 
     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 ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     ui.input(label='Text', placeholder='start typing',
              on_change=lambda e: result.set_text('you typed: ' + e.value),
              validation={'Input too long': lambda value: len(value) < 20})
     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') \
             .on('update:model-value', lambda msg: ui.notify(f'{msg["args"]}'),
                 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>
         ''')
         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 ..documentation_tools import text_demo
+
 
 def main_demo() -> None:
     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)