Преглед изворни кода

Merge branch 'main' into markbaumgarten/main

Falko Schindler пре 2 година
родитељ
комит
bc3e015c31

+ 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]

+ 50 - 38
main.py

@@ -10,6 +10,7 @@ if True:
 
 
 import os
 import os
 from pathlib import Path
 from pathlib import Path
+from typing import Optional
 
 
 from fastapi import Request
 from fastapi import Request
 from fastapi.responses import FileResponse, RedirectResponse
 from fastapi.responses import FileResponse, RedirectResponse
@@ -61,7 +62,7 @@ def add_head_html() -> None:
     ui.add_head_html(f"<style>{(Path(__file__).parent / 'website' / 'static' / 'style.css').read_text()}</style>")
     ui.add_head_html(f"<style>{(Path(__file__).parent / 'website' / 'static' / 'style.css').read_text()}</style>")
 
 
 
 
-def add_header() -> None:
+def add_header(menu: Optional[ui.left_drawer] = None) -> None:
     menu_items = {
     menu_items = {
         'Installation': '/#installation',
         'Installation': '/#installation',
         'Features': '/#features',
         'Features': '/#features',
@@ -73,14 +74,12 @@ def add_header() -> None:
     with ui.header() \
     with ui.header() \
             .classes('items-center duration-200 p-0 px-4 no-wrap') \
             .classes('items-center duration-200 p-0 px-4 no-wrap') \
             .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
             .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
+        if menu:
+            ui.button(on_click=menu.toggle).props('flat color=white icon=menu round') \
+                .classes('max-[405px]:hidden lg:hidden')
         with ui.link(target=index_page).classes('row gap-4 items-center no-wrap mr-auto'):
         with ui.link(target=index_page).classes('row gap-4 items-center no-wrap mr-auto'):
             svg.face().classes('w-8 stroke-white stroke-2')
             svg.face().classes('w-8 stroke-white stroke-2')
             svg.word().classes('w-24')
             svg.word().classes('w-24')
-        with ui.row().classes('lg:hidden'):
-            with ui.menu().classes('bg-primary text-white text-lg') as menu:
-                for title, target in menu_items.items():
-                    ui.menu_item(title, on_click=lambda _, target=target: ui.open(target))
-            ui.button(on_click=menu.open).props('flat color=white icon=menu')
         with ui.row().classes('max-lg:hidden'):
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
                 ui.link(title, target).classes(replace='text-lg text-white')
@@ -88,7 +87,12 @@ def add_header() -> None:
             svg.discord().classes('fill-white scale-125 m-1')
             svg.discord().classes('fill-white scale-125 m-1')
         with ui.link(target='https://github.com/zauberzeug/nicegui/'):
         with ui.link(target='https://github.com/zauberzeug/nicegui/'):
             svg.github().classes('fill-white scale-125 m-1')
             svg.github().classes('fill-white scale-125 m-1')
-        add_star()
+        add_star().classes('max-[460px]:hidden')
+        with ui.row().classes('lg:hidden'):
+            with ui.button().props('flat color=white icon=more_vert round'):
+                with ui.menu().classes('bg-primary text-white text-lg').props(remove='no-parent-event'):
+                    for title, target in menu_items.items():
+                        ui.menu_item(title, on_click=lambda _, target=target: ui.open(target))
 
 
 
 
 @ui.page('/')
 @ui.page('/')
@@ -136,18 +140,25 @@ async def index_page(client: Client):
                 ui.html('<em>1.</em>').classes('text-3xl font-bold')
                 ui.html('<em>1.</em>').classes('text-3xl font-bold')
                 ui.markdown('Create __main.py__').classes('text-lg')
                 ui.markdown('Create __main.py__').classes('text-lg')
                 with python_window(classes='w-full h-52'):
                 with python_window(classes='w-full h-52'):
-                    ui.markdown('''```python\n
-from nicegui import ui
+                    ui.markdown('''
+                        ```python\n
+                        from nicegui import ui
 
 
-ui.label('Hello NiceGUI!')
+                        ui.label('Hello NiceGUI!')
 
 
-ui.run()
-```''')
+                        ui.run()
+                        ```
+                    ''')
             with ui.column().classes('w-full max-w-md gap-2'):
             with ui.column().classes('w-full max-w-md gap-2'):
                 ui.html('<em>2.</em>').classes('text-3xl font-bold')
                 ui.html('<em>2.</em>').classes('text-3xl font-bold')
                 ui.markdown('Install and launch').classes('text-lg')
                 ui.markdown('Install and launch').classes('text-lg')
                 with bash_window(classes='w-full h-52'):
                 with bash_window(classes='w-full h-52'):
-                    ui.markdown('```bash\npip3 install nicegui\npython3 main.py\n```')
+                    ui.markdown('''
+                        ```bash
+                        pip3 install nicegui
+                        python3 main.py
+                        ```
+                    ''')
             with ui.column().classes('w-full max-w-md gap-2'):
             with ui.column().classes('w-full max-w-md gap-2'):
                 ui.html('<em>3.</em>').classes('text-3xl font-bold')
                 ui.html('<em>3.</em>').classes('text-3xl font-bold')
                 ui.markdown('Enjoy!').classes('text-lg')
                 ui.markdown('Enjoy!').classes('text-lg')
@@ -156,15 +167,18 @@ ui.run()
         with ui.expansion('...or use Docker to run your main.py').classes('w-full gap-2 bold-links arrow-links'):
         with ui.expansion('...or use Docker to run your main.py').classes('w-full gap-2 bold-links arrow-links'):
             with ui.row().classes('mt-8 w-full justify-center items-center gap-8'):
             with ui.row().classes('mt-8 w-full justify-center items-center gap-8'):
                 ui.markdown('''
                 ui.markdown('''
-With our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) 
-you can start the server without installing any packages.
+                    With our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) 
+                    you can start the server without installing any packages.
 
 
-The command searches for `main.py` in in your current directory and makes the app available at http://localhost:8888.
-''').classes('max-w-xl')
+                    The command searches for `main.py` in in your current directory and makes the app available at http://localhost:8888.
+                ''').classes('max-w-xl')
                 with bash_window(classes='max-w-lg w-full h-52'):
                 with bash_window(classes='max-w-lg w-full h-52'):
-                    ui.markdown('```bash\n'
-                                'docker run -it --rm -p 8888:8080 \\\n -v "$PWD":/app zauberzeug/nicegui\n'
-                                '```')
+                    ui.markdown('''
+                        ```bash
+                        docker run -it --rm -p 8888:8080 \\
+                            -v "$PWD":/app zauberzeug/nicegui
+                        ```
+                    ''')
 
 
     with ui.column().classes('w-full p-8 lg:p-16 bold-links arrow-links max-w-[1600px] mx-auto'):
     with ui.column().classes('w-full p-8 lg:p-16 bold-links arrow-links max-w-[1600px] mx-auto'):
         link_target('features', '-50px')
         link_target('features', '-50px')
@@ -229,18 +243,16 @@ The command searches for `main.py` in in your current directory and makes the ap
         with ui.row().classes('w-full text-lg leading-tight grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4'):
         with ui.row().classes('w-full text-lg leading-tight grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4'):
             example_link('Slideshow', 'implements a keyboard-controlled image slideshow')
             example_link('Slideshow', 'implements a keyboard-controlled image slideshow')
             example_link('Authentication', 'shows how to use sessions to build a login screen')
             example_link('Authentication', 'shows how to use sessions to build a login screen')
-            example_link(
-                'Modularization',
-                'provides an example of how to modularize your application into multiple files and reuse code')
-            example_link(
-                'FastAPI',
-                'illustrates the integration of NiceGUI with an existing FastAPI application')
-            example_link(
-                'Map',
-                'demonstrates wrapping the JavaScript library [leaflet](https://leafletjs.com/) to display a map at specific locations')
-            example_link(
-                'AI Interface',
-                'utilizes the [replicate](https://replicate.com) library to perform voice-to-text transcription and generate images from prompts with Stable Diffusion')
+            example_link('Modularization',
+                         'provides an example of how to modularize your application into multiple files and reuse code')
+            example_link('FastAPI',
+                         'illustrates the integration of NiceGUI with an existing FastAPI application')
+            example_link('Map',
+                         'demonstrates wrapping the JavaScript library [leaflet](https://leafletjs.com/) '
+                         'to display a map at specific locations')
+            example_link('AI Interface',
+                         'utilizes the [replicate](https://replicate.com) library to perform voice-to-text '
+                         'transcription and generate images from prompts with Stable Diffusion')
             example_link('3D Scene', 'creates a webGL view and loads an STL mesh illuminated with a spotlight')
             example_link('3D Scene', 'creates a webGL view and loads an STL mesh illuminated with a spotlight')
             example_link('Custom Vue Component', 'shows how to write and integrate a custom Vue component')
             example_link('Custom Vue Component', 'shows how to write and integrate a custom Vue component')
             example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
             example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
@@ -300,15 +312,15 @@ The command searches for `main.py` in in your current directory and makes the ap
 @ui.page('/documentation')
 @ui.page('/documentation')
 def documentation_page():
 def documentation_page():
     add_head_html()
     add_head_html()
-    add_header()
-    side_menu()
+    menu = side_menu()
+    add_header(menu)
     ui.add_head_html('<style>html {scroll-behavior: auto;}</style>')
     ui.add_head_html('<style>html {scroll-behavior: auto;}</style>')
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
         section_heading('Reference, Demos and more', '*NiceGUI* Documentation')
         section_heading('Reference, Demos and more', '*NiceGUI* Documentation')
-        ui.markdown(
-            'This is the documentation for NiceGUI >= 1.0. '
-            'Documentation for older versions can be found at [https://0.9.nicegui.io/](https://0.9.nicegui.io/reference).'
-        ).classes('bold-links arrow-links')
+        ui.markdown('''
+            This is the documentation for NiceGUI >= 1.0.
+            Documentation for older versions can be found at [https://0.9.nicegui.io/](https://0.9.nicegui.io/reference).
+        ''').classes('bold-links arrow-links')
         documentation.create_full()
         documentation.create_full()
 
 
 
 

+ 21 - 7
nicegui/binding.py

@@ -2,7 +2,7 @@ import asyncio
 import logging
 import logging
 import time
 import time
 from collections import defaultdict
 from collections import defaultdict
-from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Type
+from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Type, Union
 
 
 from . import globals
 from . import globals
 
 
@@ -11,15 +11,29 @@ bindable_properties: Dict[Tuple[int, str], Any] = {}
 active_links: List[Tuple[Any, str, Any, str, Callable]] = []
 active_links: List[Tuple[Any, str, Any, str, Callable]] = []
 
 
 
 
+def get_attribute(obj: Union[object, Dict], name: str) -> Any:
+    if isinstance(obj, dict):
+        return obj[name]
+    else:
+        return getattr(obj, name)
+
+
+def set_attribute(obj: Union[object, Dict], name: str, value: Any) -> None:
+    if isinstance(obj, dict):
+        obj[name] = value
+    else:
+        setattr(obj, name, value)
+
+
 async def loop():
 async def loop():
     while True:
     while True:
         visited: Set[Tuple[int, str]] = set()
         visited: Set[Tuple[int, str]] = set()
         t = time.time()
         t = time.time()
         for link in active_links:
         for link in active_links:
             (source_obj, source_name, target_obj, target_name, transform) = link
             (source_obj, source_name, target_obj, target_name, transform) = link
-            value = transform(getattr(source_obj, source_name))
-            if getattr(target_obj, target_name) != value:
-                setattr(target_obj, target_name, value)
+            value = transform(get_attribute(source_obj, source_name))
+            if get_attribute(target_obj, target_name) != value:
+                set_attribute(target_obj, target_name, value)
                 propagate(target_obj, target_name, visited)
                 propagate(target_obj, target_name, visited)
             del link, source_obj, target_obj
             del link, source_obj, target_obj
         if time.time() - t > 0.01:
         if time.time() - t > 0.01:
@@ -34,9 +48,9 @@ def propagate(source_obj: Any, source_name: str, visited: Optional[Set[Tuple[int
     for _, target_obj, target_name, transform in bindings.get((id(source_obj), source_name), []):
     for _, target_obj, target_name, transform in bindings.get((id(source_obj), source_name), []):
         if (id(target_obj), target_name) in visited:
         if (id(target_obj), target_name) in visited:
             continue
             continue
-        target_value = transform(getattr(source_obj, source_name))
-        if getattr(target_obj, target_name) != target_value:
-            setattr(target_obj, target_name, target_value)
+        target_value = transform(get_attribute(source_obj, source_name))
+        if get_attribute(target_obj, target_name) != target_value:
+            set_attribute(target_obj, target_name, target_value)
             propagate(target_obj, target_name, visited)
             propagate(target_obj, target_name, visited)
 
 
 
 

+ 1 - 0
nicegui/element.py

@@ -227,6 +227,7 @@ class Element(Visibility):
                 trailing_events=trailing_events,
                 trailing_events=trailing_events,
             )
             )
             self._event_listeners[listener.id] = listener
             self._event_listeners[listener.id] = listener
+            self.update()
         return self
         return self
 
 
     def _handle_event(self, msg: Dict) -> None:
     def _handle_event(self, msg: Dict) -> None:

+ 24 - 0
nicegui/elements/grid.py

@@ -0,0 +1,24 @@
+from typing import Optional
+
+from ..element import Element
+
+
+class Grid(Element):
+
+    def __init__(self,
+                 rows: Optional[int] = None,
+                 columns: Optional[int] = None,
+                 ) -> None:
+        '''Grid Element
+
+        Provides a container which arranges its child in a grid.
+
+        :param rows: number of rows in the grid
+        :param columns: number of columns in the grid
+        '''
+        super().__init__('div')
+        self._classes = ['nicegui-grid']
+        if rows is not None:
+            self._style['grid-template-rows'] = f'repeat({rows}, minmax(0, 1fr))'
+        if columns is not None:
+            self._style['grid-template-columns'] = f'repeat({columns}, minmax(0, 1fr))'

+ 23 - 1
nicegui/elements/input.py

@@ -1,4 +1,4 @@
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 
 from .icon import Icon
 from .icon import Icon
 from .mixins.disableable_element import DisableableElement
 from .mixins.disableable_element import DisableableElement
@@ -15,6 +15,7 @@ class Input(ValueElement, DisableableElement):
                  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[str]] = None,
                  validation: Dict[str, Callable] = {}) -> None:
                  validation: Dict[str, Callable] = {}) -> None:
         """Text Input
         """Text Input
 
 
@@ -33,6 +34,7 @@ class Input(ValueElement, DisableableElement):
         :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 value changes
         :param on_change: callback to execute when the value changes
+        :param autocomplete: optional list of strings for autocompletion
         :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)
@@ -52,6 +54,26 @@ class Input(ValueElement, DisableableElement):
 
 
         self.validation = validation
         self.validation = validation
 
 
+        if autocomplete:
+            def find_autocompletion() -> Optional[str]:
+                if self.value:
+                    for item in autocomplete:
+                        if item.startswith(self.value):
+                            return item
+
+            def autocomplete_input() -> None:
+                match = find_autocompletion() or ''
+                self.props(f'shadow-text="{match[len(self.value):]}"')
+
+            def complete_input() -> None:
+                match = find_autocompletion()
+                if match:
+                    self.set_value(match)
+                self.props(f'shadow-text=""')
+
+            self.on('keyup', autocomplete_input)
+            self.on('keydown.tab', complete_input)
+
     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():

+ 1 - 0
nicegui/elements/markdown.py

@@ -24,6 +24,7 @@ class Markdown(ContentElement):
         """
         """
         self.extras = extras
         self.extras = extras
         super().__init__(tag='markdown', content=content)
         super().__init__(tag='markdown', content=content)
+        self._classes = ['nicegui-markdown']
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
         self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
 
 
     def on_content_change(self, content: str) -> None:
     def on_content_change(self, content: str) -> None:

+ 52 - 1
nicegui/elements/mixins/disableable_element.py

@@ -1,4 +1,8 @@
-from ...binding import BindableProperty
+from typing import Any, Callable
+
+from typing_extensions import Self
+
+from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 from ...element import Element
 
 
 
 
@@ -17,6 +21,53 @@ class DisableableElement(Element):
         """Disable the element."""
         """Disable the element."""
         self.enabled = False
         self.enabled = False
 
 
+    def bind_enabled_to(self,
+                        target_object: Any,
+                        target_name: str = 'enabled',
+                        forward: Callable = lambda x: x) -> Self:
+        """Bind the enabled state of this element to the target object's target_name property.
+
+        The binding works one way only, from this element to the target.
+
+        :param target_object: The object to bind to.
+        :param target_name: The name of the property to bind to.
+        :param forward: A function to apply to the value before applying it to the target.
+        """
+        bind_to(self, 'enabled', target_object, target_name, forward)
+        return self
+
+    def bind_enabled_from(self,
+                          target_object: Any,
+                          target_name: str = 'enabled',
+                          backward: Callable = lambda x: x) -> Self:
+        """Bind the enabled state of this element from the target object's target_name property.
+
+        The binding works one way only, from the target to this element.
+
+        :param target_object: The object to bind from.
+        :param target_name: The name of the property to bind from.
+        :param backward: A function to apply to the value before applying it to this element.
+        """
+        bind_from(self, 'enabled', target_object, target_name, backward)
+        return self
+
+    def bind_enabled(self,
+                     target_object: Any,
+                     target_name: str = 'enabled', *,
+                     forward: Callable = lambda x: x,
+                     backward: Callable = lambda x: x) -> Self:
+        """Bind the enabled state of this element to the target object's target_name property.
+
+        The binding works both ways, from this element to the target and from the target to this element.
+
+        :param target_object: The object to bind to.
+        :param target_name: The name of the property to bind to.
+        :param forward: A function to apply to the value before applying it to the target.
+        :param backward: A function to apply to the value before applying it to this element.
+        """
+        bind(self, 'enabled', target_object, target_name, forward=forward, backward=backward)
+        return self
+
     def set_enabled(self, value: bool) -> None:
     def set_enabled(self, value: bool) -> None:
         """Set the enabled state of the element."""
         """Set the enabled state of the element."""
         self.enabled = value
         self.enabled = value

+ 1 - 1
nicegui/elements/number.py

@@ -62,7 +62,7 @@ class Number(ValueElement, DisableableElement):
         value = float(self.value or 0)
         value = float(self.value or 0)
         value = max(value, self._props.get('min', -float('inf')))
         value = max(value, self._props.get('min', -float('inf')))
         value = min(value, self._props.get('max', float('inf')))
         value = min(value, self._props.get('max', float('inf')))
-        self.set_value(self.format % value if self.format else str(value))
+        self.set_value(float(self.format % value) if self.format else 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)

+ 5 - 0
nicegui/elements/video.js

@@ -1,5 +1,10 @@
 export default {
 export default {
   template: `<video :controls="controls" :autoplay="autoplay" :muted="muted" :src="src" />`,
   template: `<video :controls="controls" :autoplay="autoplay" :muted="muted" :src="src" />`,
+  methods: {
+    seek(seconds) {
+      this.$el.currentTime = seconds;
+    },
+  },
   props: {
   props: {
     controls: Boolean,
     controls: Boolean,
     autoplay: Boolean,
     autoplay: Boolean,

+ 7 - 0
nicegui/elements/video.py

@@ -36,3 +36,10 @@ class Video(Element):
         if type:
         if type:
             url = f'https://github.com/zauberzeug/nicegui/pull/624'
             url = f'https://github.com/zauberzeug/nicegui/pull/624'
             warnings.warn(DeprecationWarning(f'The type parameter for ui.video is deprecated and ineffective ({url}).'))
             warnings.warn(DeprecationWarning(f'The type parameter for ui.video is deprecated and ineffective ({url}).'))
+
+    def seek(self, seconds: float) -> None:
+        """Seek to a specific position in the video.
+
+        :param seconds: the position in seconds
+        """
+        self.run_method('seek', seconds)

+ 19 - 4
nicegui/favicon.py

@@ -1,4 +1,4 @@
-import urllib
+import urllib.parse
 from pathlib import Path
 from pathlib import Path
 from typing import TYPE_CHECKING, Optional
 from typing import TYPE_CHECKING, Optional
 
 
@@ -25,7 +25,11 @@ def get_favicon_url(page: 'page', prefix: str) -> str:
         return favicon
         return favicon
     elif not favicon:
     elif not favicon:
         return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
         return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
-    if is_char(favicon):
+    elif is_data_url(favicon):
+        return favicon
+    elif is_svg(favicon):
+        return svg_to_data_url(favicon)
+    elif is_char(favicon):
         return char_to_data_url(favicon)
         return char_to_data_url(favicon)
     elif page.path == '/':
     elif page.path == '/':
         return f'{prefix}/favicon.ico'
         return f'{prefix}/favicon.ico'
@@ -41,6 +45,14 @@ def is_char(favicon: str) -> bool:
     return len(favicon) == 1
     return len(favicon) == 1
 
 
 
 
+def is_svg(favicon: str) -> bool:
+    return favicon.strip().startswith('<svg')
+
+
+def is_data_url(favicon: str) -> bool:
+    return favicon.startswith('data:')
+
+
 def char_to_data_url(char: str) -> str:
 def char_to_data_url(char: str) -> str:
     svg = f'''
     svg = f'''
         <svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" >
         <svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" >
@@ -58,6 +70,9 @@ def char_to_data_url(char: str) -> str:
             <text y=".9em" font-size="128" font-family="Georgia, sans-serif">{char}</text>
             <text y=".9em" font-size="128" font-family="Georgia, sans-serif">{char}</text>
         </svg>
         </svg>
     '''
     '''
+    return svg_to_data_url(svg)
+
+
+def svg_to_data_url(svg: str) -> str:
     svg_urlencoded = urllib.parse.quote(svg)
     svg_urlencoded = urllib.parse.quote(svg)
-    data_url = f"data:image/svg+xml,{svg_urlencoded}"
-    return data_url
+    return f'data:image/svg+xml,{svg_urlencoded}'

+ 3 - 0
nicegui/functions/refreshable.js

@@ -0,0 +1,3 @@
+export default {
+  template: `<slot></slot>`,
+};

+ 5 - 2
nicegui/functions/refreshable.py

@@ -2,15 +2,18 @@ from typing import Callable, List
 
 
 from typing_extensions import Self
 from typing_extensions import Self
 
 
+from ..dependencies import register_component
 from ..element import Element
 from ..element import Element
 
 
+register_component('refreshable', __file__, 'refreshable.js')
+
 
 
 class refreshable:
 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
@@ -22,7 +25,7 @@ class refreshable:
         return self
         return self
 
 
     def __call__(self) -> None:
     def __call__(self) -> None:
-        with Element('div') as container:
+        with Element('refreshable') as container:
             self.func() if self.instance is None else self.func(self.instance)
             self.func() if self.instance is None else self.func(self.instance)
         self.containers.append(container)
         self.containers.append(container)
 
 

+ 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:

+ 2 - 1
nicegui/run_with.py

@@ -14,6 +14,7 @@ def run_with(
     dark: Optional[bool] = False,
     dark: Optional[bool] = False,
     binding_refresh_interval: float = 0.1,
     binding_refresh_interval: float = 0.1,
     exclude: str = '',
     exclude: str = '',
+    mount_path: str = '/',
 ) -> None:
 ) -> None:
     globals.ui_run_has_been_called = True
     globals.ui_run_has_been_called = True
     globals.title = title
     globals.title = title
@@ -27,4 +28,4 @@ def run_with(
     app.on_event('startup')(lambda: handle_startup(with_welcome_message=False))
     app.on_event('startup')(lambda: handle_startup(with_welcome_message=False))
     app.on_event('shutdown')(lambda: handle_shutdown())
     app.on_event('shutdown')(lambda: handle_shutdown())
 
 
-    app.mount('/', globals.app)
+    app.mount(mount_path, globals.app)

+ 9 - 0
nicegui/static/nicegui.css

@@ -36,6 +36,10 @@
   align-items: flex-start;
   align-items: flex-start;
   gap: 1rem;
   gap: 1rem;
 }
 }
+.nicegui-grid {
+  display: grid;
+  gap: 1rem;
+}
 .nicegui-card {
 .nicegui-card {
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
@@ -67,6 +71,11 @@
   opacity: 1 !important;
   opacity: 1 !important;
   cursor: text !important;
   cursor: text !important;
 }
 }
+.nicegui-markdown blockquote {
+  border-left: 0.25rem solid #8884;
+  padding: 1rem 1rem 0.5rem 1rem;
+  margin: 1rem 0;
+}
 
 
 #popup {
 #popup {
   position: fixed;
   position: fixed;

+ 1 - 0
nicegui/ui.py

@@ -21,6 +21,7 @@ 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
+from .elements.grid import Grid as grid
 from .elements.html import Html as html
 from .elements.html import Html as html
 from .elements.icon import Icon as icon
 from .elements.icon import Icon as icon
 from .elements.image import Image as image
 from .elements.image import Image as image

+ 27 - 0
tests/test_input.py

@@ -1,4 +1,5 @@
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.by import By
+from selenium.webdriver.common.keys import Keys
 
 
 from nicegui import ui
 from nicegui import ui
 
 
@@ -77,3 +78,29 @@ def test_input_with_multi_word_error_message(screen: Screen):
 
 
     screen.click('set error')
     screen.click('set error')
     screen.should_contain('Some multi word error message')
     screen.should_contain('Some multi word error message')
+
+
+def test_autocompletion(screen: Screen):
+    ui.input('Input', autocomplete=['foo', 'bar', 'baz'])
+
+    screen.open('/')
+    element = screen.selenium.find_element(By.XPATH, '//*[@aria-label="Input"]')
+    element.send_keys('f')
+    screen.should_contain('oo')
+
+    element.send_keys('l')
+    screen.wait(0.5)
+    screen.should_not_contain('oo')
+
+    element.send_keys(Keys.BACKSPACE)
+    screen.should_contain('oo')
+
+    element.send_keys(Keys.TAB)
+    assert element.get_attribute('value') == 'foo'
+
+    element.send_keys(Keys.BACKSPACE)
+    element.send_keys(Keys.BACKSPACE)
+    element.send_keys('x')
+    element.send_keys(Keys.TAB)
+    screen.wait(0.5)
+    assert element.get_attribute('value') == 'fx'

+ 35 - 35
website/demo.py

@@ -1,5 +1,5 @@
 import inspect
 import inspect
-from typing import Callable, Optional
+from typing import Callable, Optional, Union
 
 
 import isort
 import isort
 
 
@@ -19,37 +19,32 @@ def remove_prefix(text: str, prefix: str) -> str:
     return text[len(prefix):] if text.startswith(prefix) else text
     return text[len(prefix):] if text.startswith(prefix) else text
 
 
 
 
-class demo:
-
-    def __init__(self, browser_title: Optional[str] = None) -> None:
-        self.browser_title = browser_title
-
-    def __call__(self, f: Callable) -> Callable:
-        with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
-            code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
-            while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
-                del code[0]
+def demo(f: Callable) -> Callable:
+    with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
+        code = inspect.getsource(f).split('# END OF DEMO')[0].strip().splitlines()
+        while not code[0].strip().startswith('def') and not code[0].strip().startswith('async def'):
             del code[0]
             del code[0]
-            indentation = len(code[0]) - len(code[0].lstrip())
-            code = [line[indentation:] for line in code]
-            code = ['from nicegui import ui'] + [remove_prefix(line, '# ') for line in code]
-            code = ['' if line == '#' else line for line in code]
-            if not code[-1].startswith('ui.run('):
-                code.append('')
-                code.append('ui.run()')
-            code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
-            with python_window(classes='w-full max-w-[44rem]'):
-                async def copy_code():
-                    await ui.run_javascript('navigator.clipboard.writeText(`' + code + '`)', respond=False)
-                    ui.notify('Copied to clipboard', type='positive', color='primary')
-                ui.markdown(f'````python\n{code}\n````')
-                ui.icon('content_copy', size='xs') \
-                    .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
-                    .on('click', copy_code)
-            with browser_window(self.browser_title,
-                                classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
-                intersection_observer(on_intersection=f)
-        return f
+        del code[0]
+        indentation = len(code[0]) - len(code[0].lstrip())
+        code = [line[indentation:] for line in code]
+        code = ['from nicegui import ui'] + [remove_prefix(line, '# ') for line in code]
+        code = ['' if line == '#' else line for line in code]
+        if not code[-1].startswith('ui.run('):
+            code.append('')
+            code.append('ui.run()')
+        code = isort.code('\n'.join(code), no_sections=True, lines_after_imports=1)
+        with python_window(classes='w-full max-w-[44rem]'):
+            async def copy_code():
+                await ui.run_javascript('navigator.clipboard.writeText(`' + code + '`)', respond=False)
+                ui.notify('Copied to clipboard', type='positive', color='primary')
+            ui.markdown(f'````python\n{code}\n````')
+            ui.icon('content_copy', size='xs') \
+                .classes('absolute right-2 top-10 opacity-10 hover:opacity-80 cursor-pointer') \
+                .on('click', copy_code)
+        with browser_window(title=getattr(f, 'tab', None),
+                            classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
+            intersection_observer(on_intersection=f)
+    return f
 
 
 
 
 def _window_header(bgcolor: str) -> ui.row():
 def _window_header(bgcolor: str) -> ui.row():
@@ -67,16 +62,21 @@ def _title(title: str) -> None:
     ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
     ui.label(title).classes('text-sm text-gray-600 absolute left-1/2 top-[6px]').style('transform: translateX(-50%)')
 
 
 
 
-def _tab(name: str, color: str, bgcolor: str) -> None:
+def _tab(content: Union[str, Callable], color: str, bgcolor: str) -> None:
     with ui.row().classes('gap-0'):
     with ui.row().classes('gap-0'):
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-br-[6px]')
-        ui.label(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}]')
+        with ui.row().classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] bg-[{color}] items-center gap-2'):
+            if callable(content):
+                content()
+            else:
+                ui.label(content)
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
         with ui.label().classes(f'w-2 h-[24px] bg-[{color}]'):
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
             ui.label().classes(f'w-full h-full bg-[{bgcolor}] rounded-bl-[6px]')
 
 
 
 
-def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
+def window(color: str, bgcolor: str, *,
+           title: str = '', tab: Union[str, Callable] = '', classes: str = '') -> ui.column:
     with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
     with ui.card().classes(f'no-wrap bg-[{color}] rounded-xl p-0 gap-0 {classes}') \
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
             .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
         with _window_header(bgcolor):
         with _window_header(bgcolor):
@@ -96,5 +96,5 @@ def bash_window(*, classes: str = '') -> ui.card:
     return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
     return window(BASH_COLOR, BASH_BGCOLOR, title='bash', classes=classes).classes('p-2 bash-window')
 
 
 
 
-def browser_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
+def browser_window(title: Optional[Union[str, Callable]] = None, *, classes: str = '') -> ui.card:
     return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')
     return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')

+ 63 - 52
website/documentation.py

@@ -144,6 +144,7 @@ def create_full() -> None:
     load_demo(ui.card)
     load_demo(ui.card)
     load_demo(ui.column)
     load_demo(ui.column)
     load_demo(ui.row)
     load_demo(ui.row)
+    load_demo(ui.grid)
 
 
     @text_demo('Clear Containers', '''
     @text_demo('Clear Containers', '''
         To remove all elements from a row, column or card container, use the `clear()` method.
         To remove all elements from a row, column or card container, use the `clear()` method.
@@ -241,52 +242,66 @@ def create_full() -> None:
         [Quasar props](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components),
         [Quasar props](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components),
         and CSS styles affect NiceGUI elements.
         and CSS styles affect NiceGUI elements.
     ''').classes('bold-links arrow-links')
     ''').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')
+    with ui.row():
+        ui.label('Choose your favorite element from those available and start having fun!').classes('mx-auto my-auto')
+        select_element = ui.select({
+            ui.label: 'ui.label',
+            ui.checkbox: 'ui.checkbox',
+            ui.switch: 'ui.switch',
+            ui.input: 'ui.input',
+            ui.textarea: 'ui.textarea',
+            ui.button: 'ui.button',
+        }, value=ui.button, on_change=lambda: live_demo_ui.refresh()).props('dense')
+
+    @ui.refreshable
+    def live_demo_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]'):
+                with ui.column().classes('w-full gap-4'):
+                    ui.markdown(f'''
+                        ```py
+                        from nicegui import ui
+
+                        element = {select_element.options[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:
+                                element.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):
+                            element._props = {'label': 'Button', 'color': 'primary'}
+                            try:
+                                element.props(e.value)
+                            except ValueError:
+                                pass
+                            element.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:
+                                element.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'):
+                element: ui.element = select_element.value("element")
+    live_demo_ui()
 
 
     @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.
@@ -632,11 +647,7 @@ def create_full() -> None:
 
 
     heading('Configuration')
     heading('Configuration')
 
 
-    @element_demo(ui.run, browser_title='My App')
-    def ui_run_demo():
-        ui.label('page with custom title')
-
-        # ui.run(title='My App')
+    load_demo(ui.run)
 
 
     # HACK: switch color to white for the next demo
     # HACK: switch color to white for the next demo
     demo_BROWSER_BGCOLOR = demo.BROWSER_BGCOLOR
     demo_BROWSER_BGCOLOR = demo.BROWSER_BGCOLOR
@@ -649,7 +660,7 @@ def create_full() -> None:
         Pick any parameter as it is defined by the internally used [pywebview module](https://pywebview.flowrl.com/guide/api.html) 
         Pick any parameter as it is defined by the internally used [pywebview module](https://pywebview.flowrl.com/guide/api.html) 
         for the `webview.create_window` and `webview.start` functions.
         for the `webview.create_window` and `webview.start` functions.
         Note that these keyword arguments will take precedence over the parameters defined in ui.run.
         Note that these keyword arguments will take precedence over the parameters defined in ui.run.
-    ''')
+    ''', tab=lambda: ui.label('NiceGUI'))
     def native_mode_demo():
     def native_mode_demo():
         from nicegui import app
         from nicegui import app
 
 

+ 6 - 5
website/documentation_tools.py

@@ -65,15 +65,17 @@ def render_docstring(doc: str, with_params: bool = True) -> ui.html:
 
 
 class text_demo:
 class text_demo:
 
 
-    def __init__(self, title: str, explanation: str) -> None:
+    def __init__(self, title: str, explanation: str, tab: Optional[Union[str, Callable]] = None) -> None:
         self.title = title
         self.title = title
         self.explanation = explanation
         self.explanation = explanation
         self.make_menu_entry = True
         self.make_menu_entry = True
+        self.tab = tab
 
 
     def __call__(self, f: Callable) -> Callable:
     def __call__(self, f: Callable) -> Callable:
         subheading(self.title, make_menu_entry=self.make_menu_entry)
         subheading(self.title, make_menu_entry=self.make_menu_entry)
         ui.markdown(self.explanation).classes('bold-links arrow-links')
         ui.markdown(self.explanation).classes('bold-links arrow-links')
-        return demo()(f)
+        f.tab = self.tab
+        return demo(f)
 
 
 
 
 class intro_demo(text_demo):
 class intro_demo(text_demo):
@@ -85,9 +87,8 @@ class intro_demo(text_demo):
 
 
 class element_demo:
 class element_demo:
 
 
-    def __init__(self, element_class: Union[Callable, type], browser_title: Optional[str] = None) -> None:
+    def __init__(self, element_class: Union[Callable, type]) -> None:
         self.element_class = element_class
         self.element_class = element_class
-        self.browser_title = browser_title
 
 
     def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
     def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
         doc = self.element_class.__doc__ or self.element_class.__init__.__doc__
         doc = self.element_class.__doc__ or self.element_class.__init__.__doc__
@@ -95,7 +96,7 @@ class element_demo:
         with ui.column().classes('w-full mb-8 gap-2'):
         with ui.column().classes('w-full mb-8 gap-2'):
             subheading(title, more_link=more_link)
             subheading(title, more_link=more_link)
             render_docstring(documentation, with_params=more_link is None)
             render_docstring(documentation, with_params=more_link is None)
-            return demo(browser_title=self.browser_title)(f)
+            return demo(f)
 
 
 
 
 def load_demo(api: Union[type, Callable]) -> None:
 def load_demo(api: Union[type, Callable]) -> None:

+ 13 - 0
website/more_documentation/grid_documentation.py

@@ -0,0 +1,13 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.grid(columns=2):
+        ui.label('Name:')
+        ui.label('Tom')
+
+        ui.label('Age:')
+        ui.label('42')
+
+        ui.label('Height:')
+        ui.label('1.80m')

+ 13 - 0
website/more_documentation/input_documentation.py

@@ -1,8 +1,21 @@
 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 autocomplete_demo():
+        options = ['AutoComplete', 'NiceGUI', 'Awesome']
+        ui.input(label='Text', placeholder='start typing', autocomplete=options)

+ 58 - 0
website/more_documentation/run_documentation.py

@@ -1,7 +1,65 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from ..documentation_tools import text_demo
+
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     ui.label('page with custom title')
     ui.label('page with custom title')
 
 
     # ui.run(title='My App')
     # ui.run(title='My App')
+main_demo.tab = 'My App'
+
+
+def more() -> None:
+    @text_demo('Emoji favicon', '''
+        You can use an emoji as favicon.
+        This works in Chrome, Firefox and Safari.
+    ''', tab=lambda: ui.markdown('🚀&nbsp; NiceGUI'))
+    def emoji_favicon():
+        ui.label('NiceGUI Rocks!')
+
+        # ui.run(favicon='🚀')
+
+    @text_demo(
+        'Base64 favicon', '''
+        You can also use an base64-encoded image as favicon.
+    ''', tab=lambda: (
+            ui.image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==')
+            .classes('w-4 h-4'),
+            ui.label('NiceGUI'),
+        ),
+    )
+    def base64_favicon():
+        ui.label('NiceGUI with a red dot!')
+
+        icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+
+        # ui.run(favicon=icon)
+
+    @text_demo('SVG favicon', '''
+        And directly use an SVG as favicon.
+        Works in Chrome, Firefox and Safari.
+    ''', tab=lambda: (
+        ui.html('''
+            <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+                <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+                <circle cx="80" cy="85" r="8" />
+                <circle cx="120" cy="85" r="8" />
+                <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+            </svg>
+        ''').classes('w-4 h-4'),
+        ui.label('NiceGUI'),
+    ))
+    def svg_favicon():
+        ui.label('NiceGUI makes you smile!')
+
+        smiley = '''
+            <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+                <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+                <circle cx="80" cy="85" r="8" />
+                <circle cx="120" cy="85" r="8" />
+                <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+            </svg>
+        '''
+
+        # ui.run(favicon=smiley)

+ 10 - 0
website/more_documentation/video_documentation.py

@@ -1,6 +1,16 @@
 from nicegui import ui
 from nicegui import ui
+from website.documentation_tools import text_demo
 
 
 
 
 def main_demo() -> None:
 def main_demo() -> None:
     v = ui.video('https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4')
     v = ui.video('https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4')
     v.on('ended', lambda _: ui.notify('Video playback completed'))
     v.on('ended', lambda _: ui.notify('Video playback completed'))
+
+
+def more() -> None:
+    @text_demo('Video start position', '''
+        This demo shows how to set the start position of a video.
+    ''')
+    def start_position_demo() -> None:
+        v = ui.video('https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_360_10s_1MB.mp4')
+        v.on('loadedmetadata', lambda: v.seek(5))

+ 4 - 3
website/star.py

@@ -5,7 +5,7 @@ from website import svg
 STYLE = '''
 STYLE = '''
 <style>
 <style>
     @keyframes star-tumble {
     @keyframes star-tumble {
-          0% { transform: translateX(2em) rotate(144deg); }
+          0% { transform: translateX(6em) rotate(432deg); }
         100% { transform: translateX(0)   rotate(0);      }
         100% { transform: translateX(0)   rotate(0);      }
     }
     }
     @keyframes star-pulse {
     @keyframes star-pulse {
@@ -43,9 +43,9 @@ STAR = '''
 '''
 '''
 
 
 
 
-def add_star() -> None:
+def add_star() -> ui.link:
     ui.add_head_html(STYLE)
     ui.add_head_html(STYLE)
-    with ui.link(target='https://github.com/zauberzeug/nicegui/').classes('star-container'):
+    with ui.link(target='https://github.com/zauberzeug/nicegui/').classes('star-container') as link:
         with Element('svg').props('viewBox="0 0 24 24"').classes('star'):
         with Element('svg').props('viewBox="0 0 24 24"').classes('star'):
             Element('path').props('d="M23.555,8.729a1.505,1.505,0,0,0-1.406-.98H16.062a.5.5,0,0,1-.472-.334L13.405,1.222a1.5,1.5,0,0,0-2.81,0l-.005.016L8.41,7.415a.5.5,0,0,1-.471.334H1.85A1.5,1.5,0,0,0,.887,10.4l5.184,4.3a.5.5,0,0,1,.155.543L4.048,21.774a1.5,1.5,0,0,0,2.31,1.684l5.346-3.92a.5.5,0,0,1,.591,0l5.344,3.919a1.5,1.5,0,0,0,2.312-1.683l-2.178-6.535a.5.5,0,0,1,.155-.543l5.194-4.306A1.5,1.5,0,0,0,23.555,8.729Z"')
             Element('path').props('d="M23.555,8.729a1.505,1.505,0,0,0-1.406-.98H16.062a.5.5,0,0,1-.472-.334L13.405,1.222a1.5,1.5,0,0,0-2.81,0l-.005.016L8.41,7.415a.5.5,0,0,1-.471.334H1.85A1.5,1.5,0,0,0,.887,10.4l5.184,4.3a.5.5,0,0,1,.155.543L4.048,21.774a1.5,1.5,0,0,0,2.31,1.684l5.346-3.92a.5.5,0,0,1,.591,0l5.344,3.919a1.5,1.5,0,0,0,2.312-1.683l-2.178-6.535a.5.5,0,0,1,.155-.543l5.194-4.306A1.5,1.5,0,0,0,23.555,8.729Z"')
         with ui.tooltip('').classes('bg-[#486991] w-96 p-4'):
         with ui.tooltip('').classes('bg-[#486991] w-96 p-4'):
@@ -54,3 +54,4 @@ def add_star() -> None:
                 with ui.column().classes('p-2 gap-2'):
                 with ui.column().classes('p-2 gap-2'):
                     ui.label('Star us on GitHub!').classes('text-[180%]')
                     ui.label('Star us on GitHub!').classes('text-[180%]')
                     ui.label('And tell others about NiceGUI.').classes('text-[140%]')
                     ui.label('And tell others about NiceGUI.').classes('text-[140%]')
+    return link