فهرست منبع

Merge branch 'main' into rewrite-audio-video-components

Falko Schindler 2 سال پیش
والد
کامیت
359c8b86b0
100فایلهای تغییر یافته به همراه2344 افزوده شده و 1391 حذف شده
  1. 25 6
      CONTRIBUTING.md
  2. 2 2
      README.md
  3. 1 1
      examples/fastapi/start.sh
  4. 44 0
      examples/ffmpeg_extract_images/main.py
  5. 1 1
      examples/modularization/main.py
  6. 1 1
      examples/modularization/theme.py
  7. 2 2
      examples/nginx_subpath/README.md
  8. 51 24
      main.py
  9. 3 11
      nicegui/element.py
  10. 7 1
      nicegui/elements/color_input.py
  11. 8 1
      nicegui/elements/color_picker.py
  12. 45 64
      nicegui/elements/interactive_image.js
  13. 0 4
      nicegui/elements/interactive_image.py
  14. 12 0
      nicegui/elements/markdown.js
  15. 14 1
      nicegui/elements/markdown.py
  16. 48 4
      nicegui/elements/mixins/content_element.py
  17. 48 4
      nicegui/elements/mixins/filter_element.py
  18. 48 4
      nicegui/elements/mixins/source_element.py
  19. 48 4
      nicegui/elements/mixins/text_element.py
  20. 48 4
      nicegui/elements/mixins/value_element.py
  21. 52 5
      nicegui/elements/mixins/visibility.py
  22. 35 0
      nicegui/elements/select.js
  23. 26 2
      nicegui/elements/select.py
  24. 2 0
      nicegui/elements/table.py
  25. 5 3
      nicegui/functions/notify.py
  26. 7 0
      tests/screen.py
  27. 14 7
      tests/test_auto_context.py
  28. 17 4
      tests/test_color_input.py
  29. 1 0
      tests/test_input.py
  30. 10 0
      tests/test_markdown.py
  31. 33 0
      tests/test_select.py
  32. 22 7
      tests/test_table.py
  33. 100 0
      website/demo.py
  34. 656 0
      website/documentation.py
  35. 188 0
      website/documentation_tools.py
  36. 0 139
      website/example.py
  37. 0 0
      website/example_card.py
  38. 0 0
      website/more_documentation/__init__.py
  39. 11 0
      website/more_documentation/add_static_files_documentation.py
  40. 71 0
      website/more_documentation/aggrid_documentation.py
  41. 9 0
      website/more_documentation/audio_documentation.py
  42. 6 0
      website/more_documentation/avatar_documentation.py
  43. 6 0
      website/more_documentation/badge_documentation.py
  44. 5 0
      website/more_documentation/button_documentation.py
  45. 8 0
      website/more_documentation/card_documentation.py
  46. 21 0
      website/more_documentation/chart_documentation.py
  47. 6 0
      website/more_documentation/checkbox_documentation.py
  48. 6 0
      website/more_documentation/circular_progress_documentation.py
  49. 7 0
      website/more_documentation/color_input_documentation.py
  50. 6 0
      website/more_documentation/color_picker_documentation.py
  51. 6 0
      website/more_documentation/colors_documentation.py
  52. 8 0
      website/more_documentation/column_documentation.py
  53. 6 0
      website/more_documentation/date_documentation.py
  54. 9 0
      website/more_documentation/dialog_documentation.py
  55. 6 0
      website/more_documentation/element_documentation.py
  56. 6 0
      website/more_documentation/expansion_documentation.py
  57. 5 0
      website/more_documentation/html_documentation.py
  58. 5 0
      website/more_documentation/icon_documentation.py
  59. 5 0
      website/more_documentation/image_documentation.py
  60. 8 0
      website/more_documentation/input_documentation.py
  61. 13 0
      website/more_documentation/interactive_image_documentation.py
  62. 8 0
      website/more_documentation/joystick_documentation.py
  63. 25 0
      website/more_documentation/keyboard_documentation.py
  64. 8 0
      website/more_documentation/knob_documentation.py
  65. 5 0
      website/more_documentation/label_documentation.py
  66. 32 0
      website/more_documentation/line_plot_documentation.py
  67. 6 0
      website/more_documentation/linear_progress_documentation.py
  68. 5 0
      website/more_documentation/link_documentation.py
  69. 8 0
      website/more_documentation/log_documentation.py
  70. 41 0
      website/more_documentation/markdown_documentation.py
  71. 14 0
      website/more_documentation/menu_documentation.py
  72. 9 0
      website/more_documentation/mermaid_documentation.py
  73. 5 0
      website/more_documentation/notify_documentation.py
  74. 7 0
      website/more_documentation/number_documentation.py
  75. 10 0
      website/more_documentation/open_documentation.py
  76. 16 0
      website/more_documentation/page_documentation.py
  77. 9 0
      website/more_documentation/plotly_documentation.py
  78. 11 0
      website/more_documentation/pyplot_documentation.py
  79. 6 0
      website/more_documentation/radio_documentation.py
  80. 8 0
      website/more_documentation/row_documentation.py
  81. 7 0
      website/more_documentation/run_documentation.py
  82. 18 0
      website/more_documentation/run_javascript_documentation.py
  83. 26 0
      website/more_documentation/scene_documentation.py
  84. 27 0
      website/more_documentation/select_documentation.py
  85. 12 0
      website/more_documentation/shutdown_documentation.py
  86. 6 0
      website/more_documentation/slider_documentation.py
  87. 8 0
      website/more_documentation/spinner_documentation.py
  88. 6 0
      website/more_documentation/switch_documentation.py
  89. 60 0
      website/more_documentation/table_documentation.py
  90. 7 0
      website/more_documentation/textarea_documentation.py
  91. 6 0
      website/more_documentation/time_documentation.py
  92. 19 0
      website/more_documentation/timer_documentation.py
  93. 6 0
      website/more_documentation/toggle_documentation.py
  94. 8 0
      website/more_documentation/tree_documentation.py
  95. 5 0
      website/more_documentation/upload_documentation.py
  96. 6 0
      website/more_documentation/video_documentation.py
  97. 0 1074
      website/reference.py
  98. 7 7
      website/static/header.html
  99. 8 4
      website/static/style.css
  100. 6 0
      website/style.py

+ 25 - 6
CONTRIBUTING.md

@@ -87,18 +87,37 @@ pytest
 
 ## Documentation
 
-New features should be well documented in [website/reference.py](https://github.com/zauberzeug/nicegui/blob/main/website/reference.py).
-By calling the `example(...)` function with an element as a parameter the docstring is used as a description.
+### Features
+
+New features should be well documented in [website/documentation.py](https://github.com/zauberzeug/nicegui/blob/main/website/documentation.py).
+By calling the `element_demo(...)` function with an element as a parameter the docstring is used as a description.
 The docstrings are written in restructured-text.
 
+### Additional Demos
+
+There is a separate page for each feature and element where multiple interactive demos can be listed.
+Please help us grow the number of insightful demos by following these easy steps:
+
+1. clone the NiceGUI repo and launch `main.py` in the root dir
+2. in the newly opened browser window you can navigate to the documentation page you want to change something
+3. open the code in your editor (for example [website/more_documentation/table_documentation.py](https://github.com/zauberzeug/nicegui/blob/main/website/more_documentation/table_documentation.py))
+4. in the `more()` function insert an inner function containing your demo code
+5. add the `@text_demo` decorator to explain the demo
+6. make sure the result looks as you expect in the rendered documentation
+7. create a pull-request (see below)
+
+Your contributions are much appreciated.
+
+### Formatting
+
 Because it has [numerous benefits](https://nick.groenen.me/notes/one-sentence-per-line/) we write each sentence in a new line.
 
-### Demos
+### Examples
 
-Besides the documentation/reference (see above) we collect useful, but compact demonstrations.
-Each demo should be about one concept.
+Besides the documentation with interactive demos (see above) we collect useful, compact stand-alone examples.
+Each example should be about one concept.
 Please try to make them as minimal as possible to show what is needed to get some kind of functionality.
-We are happy to merge pull requests with new demos which show new concepts, ideas or interesting use cases.
+We are happy to merge pull requests with new examples which show new concepts, ideas or interesting use cases.
 
 ## Pull requests
 

+ 2 - 2
README.md

@@ -75,10 +75,10 @@ Note: NiceGUI will automatically reload the page when you modify the code.
 
 ## Documentation and Examples
 
-The API reference is hosted at [https://nicegui.io/reference](https://nicegui.io/reference) and provides plenty of live examples.
+The documentation is hosted at [https://nicegui.io/documentation](https://nicegui.io/documentation) and provides plenty of live demos.
 The whole content of [https://nicegui.io](https://nicegui.io) is [implemented with NiceGUI itself](https://github.com/zauberzeug/nicegui/blob/main/main.py).
 
-You may also have a look at [our in-depth demonstrations](https://github.com/zauberzeug/nicegui/tree/main/examples) of what you can do with NiceGUI.
+You may also have a look at our [in-depth examples](https://github.com/zauberzeug/nicegui/tree/main/examples) of what you can do with NiceGUI.
 
 ## Why?
 

+ 1 - 1
examples/fastapi/start.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env bash
 
-# use path of this demo as working directory; enables starting this script from anywhere
+# use path of this example as working directory; enables starting this script from anywhere
 cd "$(dirname "$0")"
 
 if [ "$1" = "prod" ]; then

+ 44 - 0
examples/ffmpeg_extract_images/main.py

@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+import asyncio
+import os
+import pathlib
+import shlex
+import shutil
+import subprocess
+
+from nicegui import app, events, ui
+
+
+def extract(source: str):
+    subprocess.call(shlex.split(f'ffmpeg -i "{source}" -vf fps=1 out_%04d.jpg'))
+
+
+async def handle_upload(args: events.UploadEventArguments):
+    if 'video' in args.type:
+        shutil.rmtree('data', ignore_errors=True)
+        os.makedirs('data', exist_ok=True)
+        os.chdir('data')
+        with open(args.name, 'wb') as f:
+            f.write(args.content.read())
+            results.clear()
+            with results:
+                ui.spinner('dots', size='xl')
+            await asyncio.to_thread(extract, args.name)
+            results.clear()
+            with results:
+                for path in pathlib.Path('.').glob('*.jpg'):
+                    ui.image(f'/data/{path.name}').classes('w-96 drop-shadow-md rounded')
+        os.chdir('..')
+    else:
+        ui.notify('Please upload a video file')
+    upload.run_method('reset')
+
+os.makedirs('data', exist_ok=True)
+app.add_static_files('/data', 'data')
+
+with ui.column().classes('w-full items-center'):
+    ui.label('Extract images from video').classes('text-3xl m-3')
+    upload = ui.upload(label='pick a video file', auto_upload=True, on_upload=handle_upload)
+    results = ui.row().classes('w-full justify-center mt-6')
+
+ui.run()

+ 1 - 1
examples/modularization/main.py

@@ -16,4 +16,4 @@ def index_page() -> None:
 # this call shows that you can also move the whole page creation into a separate file
 example_pages.create()
 
-ui.run(title='Modularization Demo')
+ui.run(title='Modularization Example')

+ 1 - 1
examples/modularization/theme.py

@@ -10,7 +10,7 @@ def frame(navtitle: str):
     '''Custom page frame to share the same styling and behavior across all pages'''
     ui.colors(primary='#6E93D6', secondary='#53B689', accent='#111B1E', positive='#53B689')
     with ui.header().classes('justify-between text-white'):
-        ui.label('Modularization Demo').classes('font-bold')
+        ui.label('Modularization Example').classes('font-bold')
         ui.label(navtitle)
         with ui.row():
             menu()

+ 2 - 2
examples/nginx_subpath/README.md

@@ -1,8 +1,8 @@
 # Serving an App Behind a Reverse Proxy Subpath
 
-This demo shows how to serve NiceGUI on a subpath behind a reverse proxy.
+This example shows how to serve NiceGUI on a subpath behind a reverse proxy.
 Here we use Nginx.
-For a Traeffic demo have a look at https://github.com/zauberzeug/nicegui/blob/main/docker-compose.yml.
+For a Traeffic example have a look at https://github.com/zauberzeug/nicegui/blob/main/docker-compose.yml.
 
 ## Try Out
 

+ 51 - 24
main.py

@@ -1,4 +1,6 @@
 #!/usr/bin/env python3
+import inspect
+import importlib
 
 if True:
     # increasing max decode packets to be able to transfer images
@@ -9,22 +11,23 @@ if True:
 import os
 from pathlib import Path
 
-from fastapi.responses import FileResponse
-from pygments.formatters import HtmlFormatter
+from fastapi import Request
+from fastapi.responses import FileResponse, RedirectResponse
 from starlette.middleware.sessions import SessionMiddleware
 
 import prometheus
 from nicegui import Client, app
 from nicegui import globals as nicegui_globals
 from nicegui import ui
-from website import demo_card, reference, svg
-from website.example import bash_window, browser_window, python_window
+from website import documentation, example_card, svg
+from website.demo import bash_window, browser_window, python_window
+from website.documentation_tools import create_anchor_name, element_demo, generate_class_doc
 from website.star import add_star
-from website.style import example_link, features, heading, link_target, section_heading, subtitle, title
+from website.style import example_link, features, heading, link_target, section_heading, side_menu, subtitle, title
 
 prometheus.start_monitor(app)
 
-# session middleware is required for demo in reference and prometheus
+# session middleware is required for demo in documentation and prometheus
 app.add_middleware(SessionMiddleware, secret_key='NiceGUI is awesome!')
 
 app.add_static_files('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
@@ -41,6 +44,12 @@ def logo():
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
 
 
+@app.middleware('http')
+async def redirect_reference_to_documentation(request: Request, call_next):
+    if request.url.path == '/reference':
+        return RedirectResponse('/documentation')
+    return await call_next(request)
+
 # NOTE in our global fly.io deployment we need to make sure that the websocket connects back to the same instance
 fly_instance_id = os.environ.get('FLY_ALLOC_ID', '').split('-')[0]
 if fly_instance_id:
@@ -49,7 +58,6 @@ if fly_instance_id:
 
 def add_head_html() -> None:
     ui.add_head_html((Path(__file__).parent / 'website' / 'static' / 'header.html').read_text())
-    ui.add_head_html(f'<style>{HtmlFormatter(nobackground=True).get_style_defs(".codehilite")}</style>')
     ui.add_head_html(f"<style>{(Path(__file__).parent / 'website' / 'static' / 'style.css').read_text()}</style>")
 
 
@@ -57,9 +65,9 @@ def add_header() -> None:
     menu_items = {
         'Features': '/#features',
         'Installation': '/#installation',
-        'Examples': '/#examples',
-        'API Reference': '/reference',
         'Demos': '/#demos',
+        'Documentation': '/documentation',
+        'Examples': '/#examples',
         'Why?': '/#why',
     }
     with ui.header() \
@@ -116,7 +124,7 @@ async def index_page(client: Client):
                     '[PyPI package](https://pypi.org/project/nicegui/), '
                     '[Docker image](https://hub.docker.com/r/zauberzeug/nicegui) and on '
                     '[GitHub](https://github.com/zauberzeug/nicegui).')
-        demo_card.create()
+        example_card.create()
 
     with ui.column().classes('w-full p-8 lg:p-16 bold-links arrow-links max-w-[1600px] mx-auto'):
         link_target('features', '-50px')
@@ -198,24 +206,24 @@ The command searches for `main.py` in in your current directory and makes the ap
                                 '```')
 
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1600px] mx-auto'):
-        link_target('examples', '-50px')
-        section_heading('Examples', 'Try *this*')
+        link_target('demos', '-50px')
+        section_heading('Demos', 'Try *this*')
         with ui.column().classes('w-full'):
-            reference.create_intro()
+            documentation.create_intro()
 
     with ui.column().classes('dark-box p-8 lg:p-16 my-16'):
         with ui.column().classes('mx-auto items-center gap-y-8 gap-x-32 lg:flex-row'):
             with ui.column().classes('gap-1 max-lg:items-center max-lg:text-center'):
-                ui.markdown('Browse through plenty of live examples.') \
+                ui.markdown('Browse through plenty of live demos.') \
                     .classes('text-white text-2xl md:text-3xl font-medium')
                 ui.html('Fun-Fact: This whole website is also coded with NiceGUI.') \
                     .classes('text-white text-lg md:text-xl')
-            ui.link('API reference', '/reference') \
+            ui.link('Documentation', '/documentation') \
                 .classes('rounded-full mx-auto px-12 py-2 text-white bg-white font-medium text-lg md:text-xl')
 
     with ui.column().classes('w-full p-8 lg:p-16 max-w-[1600px] mx-auto'):
-        link_target('demos', '-50px')
-        section_heading('In-depth demonstrations', 'Pick your *solution*')
+        link_target('examples', '-50px')
+        section_heading('In-depth examples', 'Pick your *solution*')
         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('Authentication', 'shows how to use sessions to build a login screen')
@@ -286,20 +294,39 @@ The command searches for `main.py` in in your current directory and makes the ap
             svg.face().classes('stroke-white shrink-0 w-[200px] md:w-[300px] lg:w-[450px]')
 
 
-@ui.page('/reference')
-def reference_page():
+@ui.page('/documentation')
+def documentation_page():
     add_head_html()
     add_header()
-    menu = ui.left_drawer() \
-        .classes('column no-wrap gap-1 bg-[#eee] mt-[-20px] px-8 py-20').style('height: calc(100% + 20px) !important')
+    menu = side_menu()
     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'):
-        section_heading('Documentation and Examples', '*API* Reference')
+        section_heading('Reference, Demos and more', '*NiceGUI* Documentation')
         ui.markdown(
-            'This is the API reference for NiceGUI >= 1.0. '
+            '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')
-        reference.create_full(menu)
+        documentation.create_full(menu)
+
+
+@ui.page('/documentation/{name}')
+def documentation_page_more(name: str):
+    add_head_html()
+    add_header()
+    with side_menu():
+        ui.markdown(f'[← back](/documentation#{create_anchor_name(name)})').classes('bold-links')
+    with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
+        if not hasattr(ui, name):
+            name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
+        section_heading('Documentation', f'ui.*{name}*')
+        module = importlib.import_module(f'website.more_documentation.{name}_documentation')
+        element_class = getattr(ui, name)
+        element_demo(element_class)(getattr(module, 'main_demo'))
+        if inspect.isclass(element_class):
+            generate_class_doc(element_class)
+        if hasattr(module, 'more'):
+            ui.markdown('## More demos').classes('mt-16')
+            getattr(module, 'more')()
 
 
 ui.run(uvicorn_reload_includes='*.py, *.css, *.html')

+ 3 - 11
nicegui/element.py

@@ -1,7 +1,6 @@
 from __future__ import annotations
 
 import re
-from abc import ABC
 from copy import deepcopy
 from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
 
@@ -20,12 +19,13 @@ if TYPE_CHECKING:
 PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
 
 
-class Element(ABC, Visibility):
+class Element(Visibility):
 
     def __init__(self, tag: str, *, _client: Optional[Client] = None) -> None:
         """Generic Element
 
-        This class is also the base class for all other elements.
+        This class is the base class for all other UI elements.
+        But you can use it to create elements with arbitrary HTML tags.
 
         :param tag: HTML tag of the element
         :param _client: client for this element (for internal use only)
@@ -167,10 +167,6 @@ class Element(ABC, Visibility):
 
         Removing or replacing styles can be helpful if the predefined style is not desired.
 
-        .. codeblock:: python
-
-            ui.button('Click me').style('color: #6E93D6; font-size: 200%', remove='font-weight; background-color')
-
         :param add: semicolon-separated list of styles to add to the element
         :param remove: semicolon-separated list of styles to remove from the element
         :param replace: semicolon-separated list of styles to use instead of existing ones
@@ -203,10 +199,6 @@ class Element(ABC, Visibility):
         This allows modifying the look of the element or its layout using `Quasar <https://quasar.dev/>`_ props.
         Since props are simply applied as HTML attributes, they can be used with any HTML element.
 
-        .. codeblock:: python
-
-            ui.button('Open menu').props('outline icon=menu')
-
         Boolean properties are assumed ``True`` if no value is specified.
 
         :param add: whitespace-delimited list of either boolean values or key=value pair to add

+ 7 - 1
nicegui/elements/color_input.py

@@ -26,5 +26,11 @@ class ColorInput(ValueElement):
 
         with self.add_slot('append'):
             self.picker = ColorPicker(on_pick=lambda e: self.set_value(e.color))
-            self.button = ui.button(on_click=self.picker.open) \
+            self.button = ui.button(on_click=self.open_picker) \
                 .props('icon=colorize flat round', remove='color').classes('cursor-pointer')
+
+    def open_picker(self) -> None:
+        """Open the color picker"""
+        if self.value:
+            self.picker.set_color(self.value)
+        self.picker.open()

+ 8 - 1
nicegui/elements/color_picker.py

@@ -18,4 +18,11 @@ class ColorPicker(Menu):
         with self:
             def handle_change(msg: Dict):
                 handle_event(on_pick, ColorPickEventArguments(sender=self, client=self.client, color=msg['args']))
-            Element('q-color').on('change', handle_change)
+            self.q_color = Element('q-color').on('change', handle_change)
+
+    def set_color(self, color: str) -> None:
+        """Set the color of the picker
+
+        :param color: the color to set
+        """
+        self.q_color.props(f'model-value="{color}"')

+ 45 - 64
nicegui/elements/interactive_image.js

@@ -1,78 +1,59 @@
 export default {
   template: `
     <div style="position:relative">
-      <img style="width:100%; height:100%" />
-      <svg style="position:absolute;top:0;left:0;pointer-events:none">
-        <g style="display:none">
-          <line x1="100" y1="0" x2="100" y2="100%" stroke="black" />
-          <line x1="0" y1="100" x2="100%" y2="100" stroke="black" />
+      <img :src="src" style="width:100%; height:100%;" v-on="onEvents" draggable="false" />
+      <svg v-if="cross" style="position:absolute;top:0;left:0;pointer-events:none" :viewBox="viewBox">
+        <g :style="{ display: cssDisplay }">
+          <line :x1="x" y1="0" :x2="x" y2="100%" stroke="black" />
+          <line x1="0" :y1="y" x2="100%" :y2="y" stroke="black" />
         </g>
-        <g v-html="content" style="display:none"></g>
+        <g v-html="content"></g>
       </svg>
     </div>
   `,
-  mounted() {
-    this.image = this.$el.firstChild;
-    const handle_completion = () => {
-      if (this.waiting_source) {
-        this.image.src = this.waiting_source;
-        this.waiting_source = undefined;
-      } else {
-        this.loading = false;
-      }
-    };
-    this.image.addEventListener("load", handle_completion);
-    this.image.addEventListener("error", handle_completion);
-    this.svg = this.$el.lastChild;
-    const cross = this.svg.firstChild;
-    this.image.ondragstart = () => false;
-    if (this.cross) {
-      this.image.style.cursor = "none";
-      this.image.addEventListener("mouseenter", (e) => {
-        cross.style.display = "block";
-      });
-      this.image.addEventListener("mouseleave", (e) => {
-        cross.style.display = "none";
-      });
-      this.image.addEventListener("mousemove", (e) => {
-        const x = (e.offsetX * e.target.naturalWidth) / e.target.clientWidth;
-        const y = (e.offsetY * e.target.naturalHeight) / e.target.clientHeight;
-        cross.firstChild.setAttribute("x1", x);
-        cross.firstChild.setAttribute("x2", x);
-        cross.lastChild.setAttribute("y1", y);
-        cross.lastChild.setAttribute("y2", y);
-      });
-    }
-    this.image.onload = (e) => {
-      const viewBox = `0 0 ${this.image.naturalWidth} ${this.image.naturalHeight}`;
-      this.svg.setAttribute("viewBox", viewBox);
-      this.svg.lastChild.setAttribute("style", "");
+  data() {
+    return {
+      viewBox: "0 0 0 0",
+      x: 100,
+      y: 100,
+      cssDisplay: "none",
     };
-    this.image.src = this.src;
-    for (const type of this.events) {
-      this.image.addEventListener(type, (e) => {
-        this.$emit("mouse", {
-          mouse_event_type: type,
-          image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
-          image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
-          button: e.button,
-          buttons: e.buttons,
-          altKey: e.altKey,
-          ctrlKey: e.ctrlKey,
-          metaKey: e.metaKey,
-          shiftKey: e.shiftKey,
-        });
-      });
-    }
   },
   methods: {
-    set_source(source) {
-      if (this.loading) {
-        this.waiting_source = source;
-        return;
+    updateCrossHair(e) {
+      this.x = (e.offsetX * e.target.naturalWidth) / e.target.clientWidth;
+      this.y = (e.offsetY * e.target.naturalHeight) / e.target.clientHeight;
+    },
+    onImageLoaded(e) {
+      this.viewBox = `0 0 ${e.target.naturalWidth} ${e.target.naturalHeight}`;
+    },
+    onMouseEvent(type, e) {
+      this.$emit("mouse", {
+        mouse_event_type: type,
+        image_x: (e.offsetX * e.target.naturalWidth) / e.target.clientWidth,
+        image_y: (e.offsetY * e.target.naturalHeight) / e.target.clientHeight,
+        button: e.button,
+        buttons: e.buttons,
+        altKey: e.altKey,
+        ctrlKey: e.ctrlKey,
+        metaKey: e.metaKey,
+        shiftKey: e.shiftKey,
+      });
+    },
+  },
+  computed: {
+    onEvents() {
+      const allEvents = {};
+      for (const type of this.events) {
+        allEvents[type] = (event) => this.onMouseEvent(type, event);
+      }
+      if (this.cross) {
+        allEvents["mouseenter"] = () => (this.cssDisplay = "block");
+        allEvents["mouseleave"] = () => (this.cssDisplay = "none");
+        allEvents["mousemove"] = (event) => this.updateCrossHair(event);
+        allEvents["load"] = (event) => this.onImageLoaded(event);
       }
-      this.loading = true;
-      this.image.src = source;
+      return allEvents;
     },
   },
   props: {

+ 0 - 4
nicegui/elements/interactive_image.py

@@ -52,7 +52,3 @@ class InteractiveImage(SourceElement, ContentElement):
             )
             return handle_event(on_mouse, arguments)
         self.on('mouse', handle_mouse)
-
-    def on_source_change(self, source: str) -> None:
-        super().on_source_change(source)
-        self.run_method('set_source', source)

+ 12 - 0
nicegui/elements/markdown.js

@@ -1,6 +1,7 @@
 export default {
   template: `<div></div>`,
   mounted() {
+    this.ensure_codehilite_css();
     this.update(this.$el.innerHTML);
   },
   methods: {
@@ -11,6 +12,17 @@ export default {
         mermaid.render(`mermaid_${this.$el.id}_${i}`, code, (svg) => (pre.innerHTML = svg));
       });
     },
+    ensure_codehilite_css() {
+      if (!document.querySelector(`style[data-codehilite-css]`)) {
+        const style = document.createElement("style");
+        style.setAttribute("data-codehilite-css", "");
+        style.innerHTML = this.codehilite_css;
+        document.head.appendChild(style);
+      }
+    },
+  },
+  props: {
+    codehilite_css: String,
   },
 };
 

+ 14 - 1
nicegui/elements/markdown.py

@@ -4,6 +4,7 @@ from functools import lru_cache
 from typing import List
 
 import markdown2
+from pygments.formatters import HtmlFormatter
 
 from ..dependencies import register_component
 from .mixins.content_element import ContentElement
@@ -23,6 +24,7 @@ class Markdown(ContentElement):
         """
         self.extras = extras
         super().__init__(tag='markdown', content=content)
+        self._props['codehilite_css'] = HtmlFormatter(nobackground=True).get_style_defs('.codehilite')
 
     def on_content_change(self, content: str) -> None:
         html = prepare_content(content, extras=' '.join(self.extras))
@@ -33,7 +35,7 @@ class Markdown(ContentElement):
 
 @lru_cache(maxsize=int(os.environ.get('MARKDOWN_CONTENT_CACHE_SIZE', '1000')))
 def prepare_content(content: str, extras: str) -> str:
-    html = markdown2.markdown(content, extras=extras.split())
+    html = markdown2.markdown(remove_indentation(content), extras=extras.split())
     return apply_tailwind(html)  # we need explicit Markdown styling because tailwind CSS removes all default styles
 
 
@@ -52,3 +54,14 @@ def apply_tailwind(html: str) -> str:
     }
     pattern = re.compile('|'.join(rep.keys()))
     return pattern.sub(lambda m: rep[re.escape(m.group(0))], html)
+
+
+def remove_indentation(text: str) -> str:
+    """Remove indentation from a multi-line string based on the indentation of the first non-empty line."""
+    lines = text.splitlines()
+    while lines and not lines[0].strip():
+        lines.pop(0)
+    if not lines:
+        return ''
+    indentation = len(lines[0]) - len(lines[0].lstrip())
+    return '\n'.join(line[indentation:] for line in lines)

+ 48 - 4
nicegui/elements/mixins/content_element.py

@@ -1,5 +1,7 @@
 from typing import Any, Callable
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 
@@ -13,23 +15,65 @@ class ContentElement(Element):
         self.content = content
         self.on_content_change(content)
 
-    def bind_content_to(self, target_object: Any, target_name: str = 'content', forward: Callable = lambda x: x):
+    def bind_content_to(self,
+                        target_object: Any,
+                        target_name: str = 'content',
+                        forward: Callable = lambda x: x) -> Self:
+        """Bind the content 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, 'content', target_object, target_name, forward)
         return self
 
-    def bind_content_from(self, target_object: Any, target_name: str = 'content', backward: Callable = lambda x: x):
+    def bind_content_from(self,
+                          target_object: Any,
+                          target_name: str = 'content',
+                          backward: Callable = lambda x: x) -> Self:
+        """Bind the content 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, 'content', target_object, target_name, backward)
         return self
 
-    def bind_content(self, target_object: Any, target_name: str = 'content', *,
-                     forward: Callable = lambda x: x, backward: Callable = lambda x: x):
+    def bind_content(self,
+                     target_object: Any,
+                     target_name: str = 'content', *,
+                     forward: Callable = lambda x: x,
+                     backward: Callable = lambda x: x) -> Self:
+        """Bind the content 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, 'content', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_content(self, content: str) -> None:
+        """Set the content of this element.
+
+        :param content: The new content.
+        """
         self.content = content
 
     def on_content_change(self, content: str) -> None:
+        """Called when the content of this element changes.
+
+        :param content: The new content.
+        """
         if self.CONTENT_PROP == 'innerHTML' and '</script>' in content:
             raise ValueError('HTML elements must not contain <script> tags. Use ui.add_body_html() instead.')
         self._props[self.CONTENT_PROP] = content

+ 48 - 4
nicegui/elements/mixins/filter_element.py

@@ -1,5 +1,7 @@
 from typing import Any, Callable, Optional
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 
@@ -13,22 +15,64 @@ class FilterElement(Element):
         self.filter = filter
         self._props[self.FILTER_PROP] = filter
 
-    def bind_filter_to(self, target_object: Any, target_name: str = 'filter', forward: Callable = lambda x: x):
+    def bind_filter_to(self,
+                       target_object: Any,
+                       target_name: str = 'filter',
+                       forward: Callable = lambda x: x) -> Self:
+        """Bind the filter 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, 'filter', target_object, target_name, forward)
         return self
 
-    def bind_filter_from(self, target_object: Any, target_name: str = 'filter', backward: Callable = lambda x: x):
+    def bind_filter_from(self,
+                         target_object: Any,
+                         target_name: str = 'filter',
+                         backward: Callable = lambda x: x) -> Self:
+        """Bind the filter 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, 'filter', target_object, target_name, backward)
         return self
 
-    def bind_filter(self, target_object: Any, target_name: str = 'filter', *,
-                    forward: Callable = lambda x: x, backward: Callable = lambda x: x):
+    def bind_filter(self,
+                    target_object: Any,
+                    target_name: str = 'filter', *,
+                    forward: Callable = lambda x: x,
+                    backward: Callable = lambda x: x) -> Self:
+        """Bind the filter 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, 'filter', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_filter(self, filter: str) -> None:
+        """Set the filter of this element.
+
+        :param filter: The new filter.
+        """
         self.filter = filter
 
     def on_filter_change(self, filter: str) -> None:
+        """Called when the filter of this element changes.
+
+        :param filter: The new filter.
+        """
         self._props[self.FILTER_PROP] = filter
         self.update()

+ 48 - 4
nicegui/elements/mixins/source_element.py

@@ -1,5 +1,7 @@
 from typing import Any, Callable
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 
@@ -12,22 +14,64 @@ class SourceElement(Element):
         self.source = source
         self._props['src'] = source
 
-    def bind_source_to(self, target_object: Any, target_name: str = 'source', forward: Callable = lambda x: x):
+    def bind_source_to(self,
+                       target_object: Any,
+                       target_name: str = 'source',
+                       forward: Callable = lambda x: x) -> Self:
+        """Bind the source 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, 'source', target_object, target_name, forward)
         return self
 
-    def bind_source_from(self, target_object: Any, target_name: str = 'source', backward: Callable = lambda x: x):
+    def bind_source_from(self,
+                         target_object: Any,
+                         target_name: str = 'source',
+                         backward: Callable = lambda x: x) -> Self:
+        """Bind the source 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, 'source', target_object, target_name, backward)
         return self
 
-    def bind_source(self, target_object: Any, target_name: str = 'source', *,
-                    forward: Callable = lambda x: x, backward: Callable = lambda x: x):
+    def bind_source(self,
+                    target_object: Any,
+                    target_name: str = 'source', *,
+                    forward: Callable = lambda x: x,
+                    backward: Callable = lambda x: x) -> Self:
+        """Bind the source 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, 'source', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_source(self, source: str) -> None:
+        """Set the source of this element.
+
+        :param source: The new source.
+        """
         self.source = source
 
     def on_source_change(self, source: str) -> None:
+        """Called when the source of this element changes.
+
+        :param source: The new source.
+        """
         self._props['src'] = source
         self.update()

+ 48 - 4
nicegui/elements/mixins/text_element.py

@@ -1,5 +1,7 @@
 from typing import Any, Callable
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 
@@ -12,23 +14,65 @@ class TextElement(Element):
         self.text = text
         self._text_to_model_text(text)
 
-    def bind_text_to(self, target_object: Any, target_name: str = 'text', forward: Callable = lambda x: x):
+    def bind_text_to(self,
+                     target_object: Any,
+                     target_name: str = 'text',
+                     forward: Callable = lambda x: x) -> Self:
+        """Bind the text 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, 'text', target_object, target_name, forward)
         return self
 
-    def bind_text_from(self, target_object: Any, target_name: str = 'text', backward: Callable = lambda x: x):
+    def bind_text_from(self,
+                       target_object: Any,
+                       target_name: str = 'text',
+                       backward: Callable = lambda x: x) -> Self:
+        """Bind the text 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, 'text', target_object, target_name, backward)
         return self
 
-    def bind_text(self, target_object: Any, target_name: str = 'text', *,
-                  forward: Callable = lambda x: x, backward: Callable = lambda x: x):
+    def bind_text(self,
+                  target_object: Any,
+                  target_name: str = 'text', *,
+                  forward: Callable = lambda x: x,
+                  backward: Callable = lambda x: x) -> Self:
+        """Bind the text 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, 'text', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_text(self, text: str) -> None:
+        """Set the text of this element.
+
+        :param text: The new text.
+        """
         self.text = text
 
     def on_text_change(self, text: str) -> None:
+        """Called when the text of this element changes.
+
+        :param text: The new text.
+        """
         self._text_to_model_text(text)
         self.update()
 

+ 48 - 4
nicegui/elements/mixins/value_element.py

@@ -1,5 +1,7 @@
 from typing import Any, Callable, Dict, Optional
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 from ...element import Element
 from ...events import ValueChangeEventArguments, handle_event
@@ -25,23 +27,65 @@ class ValueElement(Element):
             self._send_update_on_value_change = True
         self.on(f'update:{self.VALUE_PROP}', handle_change, self.EVENT_ARGS, throttle=throttle)
 
-    def bind_value_to(self, target_object: Any, target_name: str = 'value', forward: Callable = lambda x: x):
+    def bind_value_to(self,
+                      target_object: Any,
+                      target_name: str = 'value',
+                      forward: Callable = lambda x: x) -> Self:
+        """Bind the value 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, 'value', target_object, target_name, forward)
         return self
 
-    def bind_value_from(self, target_object: Any, target_name: str = 'value', backward: Callable = lambda x: x):
+    def bind_value_from(self,
+                        target_object: Any,
+                        target_name: str = 'value',
+                        backward: Callable = lambda x: x) -> Self:
+        """Bind the value 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, 'value', target_object, target_name, backward)
         return self
 
-    def bind_value(self, target_object: Any, target_name: str = 'value', *,
-                   forward: Callable = lambda x: x, backward: Callable = lambda x: x):
+    def bind_value(self,
+                   target_object: Any,
+                   target_name: str = 'value', *,
+                   forward: Callable = lambda x: x,
+                   backward: Callable = lambda x: x) -> Self:
+        """Bind the value 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, 'value', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_value(self, value: Any) -> None:
+        """Set the value of this element.
+
+        :param value: The value to set.
+        """
         self.value = value
 
     def on_value_change(self, value: Any) -> None:
+        """Called when the value of this element changes.
+
+        :param value: The new value.
+        """
         self._props[self.VALUE_PROP] = self._value_to_model_value(value)
         if self._send_update_on_value_change:
             self.update()

+ 52 - 5
nicegui/elements/mixins/visibility.py

@@ -1,5 +1,7 @@
 from typing import TYPE_CHECKING, Any, Callable
 
+from typing_extensions import Self
+
 from ...binding import BindableProperty, bind, bind_from, bind_to
 
 if TYPE_CHECKING:
@@ -13,28 +15,73 @@ class Visibility:
         super().__init__(**kwargs)
         self.visible = True
 
-    def bind_visibility_to(self, target_object: Any, target_name: str = 'visible', forward: Callable = lambda x: x):
+    def bind_visibility_to(self,
+                           target_object: Any,
+                           target_name: str = 'visible',
+                           forward: Callable = lambda x: x) -> Self:
+        """Bind the visibility 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, 'visible', target_object, target_name, forward)
         return self
 
-    def bind_visibility_from(self, target_object: Any, target_name: str = 'visible',
-                             backward: Callable = lambda x: x, *, value: Any = None):
+    def bind_visibility_from(self,
+                             target_object: Any,
+                             target_name: str = 'visible',
+                             backward: Callable = lambda x: x, *,
+                             value: Any = None) -> Self:
+        """Bind the visibility 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.
+        :param value: If specified, the element will be visible only when the target value is equal to this value.
+        """
         if value is not None:
             def backward(x): return x == value
         bind_from(self, 'visible', target_object, target_name, backward)
         return self
 
-    def bind_visibility(self, target_object: Any, target_name: str = 'visible', *,
-                        forward: Callable = lambda x: x, backward: Callable = lambda x: x, value: Any = None):
+    def bind_visibility(self,
+                        target_object: Any,
+                        target_name: str = 'visible', *,
+                        forward: Callable = lambda x: x,
+                        backward: Callable = lambda x: x,
+                        value: Any = None) -> Self:
+        """Bind the visibility 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.
+        :param value: If specified, the element will be visible only when the target value is equal to this value.
+        """
         if value is not None:
             def backward(x): return x == value
         bind(self, 'visible', target_object, target_name, forward=forward, backward=backward)
         return self
 
     def set_visibility(self, visible: str) -> None:
+        """Set the visibility of this element.
+
+        :param visible: Whether the element should be visible.
+        """
         self.visible = visible
 
     def on_visibility_change(self: 'Element', visible: str) -> None:
+        """Called when the visibility of this element changes.
+
+        :param visible: Whether the element should be visible.
+        """
         if visible and 'hidden' in self._classes:
             self._classes.remove('hidden')
             self.update()

+ 35 - 0
nicegui/elements/select.js

@@ -0,0 +1,35 @@
+export default {
+  props: ["options"],
+  template: `
+      <q-select v-bind="$attrs" :options="filteredOptions" @filter="filterFn">
+          <template v-for="(_, slot) in $slots" v-slot:[slot]="slotProps">
+              <slot :name="slot" v-bind="slotProps || {}" />
+          </template>
+      </q-select>
+    `,
+  data() {
+    return {
+      initialOptions: this.options,
+      filteredOptions: this.options,
+    };
+  },
+  methods: {
+    filterFn(val, update, abort) {
+      update(() => {
+        const needle = val.toLocaleLowerCase();
+        this.filteredOptions = needle
+          ? this.initialOptions.filter((v) => String(v.label).toLocaleLowerCase().indexOf(needle) > -1)
+          : this.initialOptions;
+      });
+    },
+  },
+  watch: {
+    options: {
+      handler(newOptions) {
+        this.initialOptions = newOptions;
+        this.filteredOptions = newOptions;
+      },
+      immediate: true,
+    },
+  },
+};

+ 26 - 2
nicegui/elements/select.py

@@ -1,12 +1,21 @@
+import re
+from copy import deepcopy
 from typing import Any, Callable, Dict, List, Optional, Union
 
+from nicegui.dependencies import register_component
+
 from .choice_element import ChoiceElement
 
+register_component('select', __file__, 'select.js')
+
 
 class Select(ChoiceElement):
 
     def __init__(self, options: Union[List, Dict], *,
-                 label: Optional[str] = None, value: Any = None, on_change: Optional[Callable] = None) -> None:
+                 label: Optional[str] = None,
+                 value: Any = None,
+                 on_change: Optional[Callable] = None,
+                 with_input: bool = False) -> None:
         """Dropdown Selection
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.
@@ -15,9 +24,24 @@ class Select(ChoiceElement):
         :param options: a list ['value1', ...] or dictionary `{'value1':'label1', ...}` specifying the options
         :param value: the initial value
         :param on_change: callback to execute when selection changes
+        :param with_input: whether to show an input field to filter the options
         """
-        super().__init__(tag='q-select', options=options, value=value, on_change=on_change)
+        super().__init__(tag='select', options=options, value=value, on_change=on_change)
         self._props['label'] = label
+        if with_input:
+            self.original_options = deepcopy(options)
+            self._props['use-input'] = True
+            self._props['hide-selected'] = True
+            self._props['fill-input'] = True
+            self._props['input-debounce'] = 0
+
+    def on_filter(self, event: Dict) -> None:
+        self.options = [
+            option
+            for option in self.original_options
+            if not event['args'] or re.search(event['args'], option, re.IGNORECASE)
+        ]
+        self.update()
 
     def _msg_to_value(self, msg: Dict) -> Any:
         return self._values[msg['args']['value']]

+ 2 - 0
nicegui/elements/table.py

@@ -49,6 +49,8 @@ class Table(FilterElement):
 
         def handle_selection(msg: Dict) -> None:
             if msg['args']['added']:
+                if selection == 'single':
+                    self.selected.clear()
                 self.selected.extend(msg['args']['rows'])
             else:
                 self.selected[:] = [row for row in self.selected if row[row_key] not in msg['args']['keys']]

+ 5 - 3
nicegui/functions/notify.py

@@ -1,12 +1,14 @@
 from typing import Any, Optional, Union
 
+from typing_extensions import Literal
+
 from .. import globals, outbox
 
 
 def notify(message: Any, *,
-           position: str = 'bottom',
+           position: Literal['top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right', 'center'] = 'bottom',
            closeBtn: Union[bool, str] = False,
-           type: Optional[str] = None,
+           type: Optional[Literal['positive', 'negative', 'warning', 'info', 'ongoing']] = None,
            color: Optional[str] = None,
            **kwargs,
            ) -> None:
@@ -15,7 +17,7 @@ def notify(message: Any, *,
     Displays a notification on the screen.
 
     :param message: content of the notification
-    :param position: position on the screen ("top-left", "top-right", "bottom-left", "bottom-right, "top", "bottom", "left", "right" or "center", default: "bottom")
+    :param position: position on the screen ("top-left", "top-right", "bottom-left", "bottom-right", "top", "bottom", "left", "right" or "center", default: "bottom")
     :param closeBtn: optional label of a button to dismiss the notification (default: `False`)
     :param type: optional type ("positive", "negative", "warning", "info" or "ongoing")
     :param color: optional color name

+ 7 - 0
tests/screen.py

@@ -1,6 +1,7 @@
 import os
 import threading
 import time
+from contextlib import contextmanager
 from typing import List
 
 import pytest
@@ -164,3 +165,9 @@ class Screen:
             assert record.message.strip() == message, f'Expected "{message}" but got "{record.message}"'
         finally:
             self.caplog.records.clear()
+
+    @contextmanager
+    def implicitly_wait(self, t: float) -> None:
+        self.selenium.implicitly_wait(t)
+        yield
+        self.selenium.implicitly_wait(self.IMPLICIT_WAIT)

+ 14 - 7
tests/test_auto_context.py

@@ -27,23 +27,22 @@ def test_adding_element_to_private_page(screen: Screen):
 
 def test_adding_elements_with_async_await(screen: Screen):
     async def add_a():
-        await asyncio.sleep(0.1)
+        await asyncio.sleep(1.0)
         ui.label('A')
 
     async def add_b():
-        await asyncio.sleep(0.1)
+        await asyncio.sleep(1.0)
         ui.label('B')
 
-    ui.label('ready')
     with ui.card() as cardA:
         ui.timer(1.0, add_a, once=True)
     with ui.card() as cardB:
-        ui.timer(1.1, add_b, once=True)
+        ui.timer(1.5, add_b, once=True)
 
     screen.open('/')
-    screen.wait_for('ready')
-    screen.should_contain('A')
-    screen.should_contain('B')
+    with screen.implicitly_wait(10.0):
+        screen.should_contain('A')
+        screen.should_contain('B')
     cA = screen.selenium.find_element(By.ID, cardA.id)
     cA.find_element(By.XPATH, './/*[contains(text(), "A")]')
     cB = screen.selenium.find_element(By.ID, cardB.id)
@@ -98,7 +97,11 @@ def test_autoupdate_on_async_timer_callback(screen: Screen):
     ui.label('0')
     ui.timer(2.0, update, once=True)
 
+    ui.timer(0, lambda: ui.label('connection established'), once=True)  # HACK: allow waiting for client connection
+
     screen.open('/')
+    with screen.implicitly_wait(10.0):
+        screen.wait_for('connection established')
     screen.should_contain('0')
     screen.should_not_contain('1')
     screen.wait_for('1')
@@ -120,7 +123,11 @@ def test_adding_elements_from_different_tasks(screen: Screen):
             ui.label('2')
             await asyncio.sleep(0.5)
 
+    ui.timer(0, lambda: ui.label('connection established'), once=True)  # HACK: allow waiting for client connection
+
     screen.open('/')
+    with screen.implicitly_wait(10.0):
+        screen.wait_for('connection established')
     background_tasks.create(add_label1())
     background_tasks.create(add_label2())
     screen.should_contain('1')

+ 17 - 4
tests/test_color_input.py

@@ -1,3 +1,4 @@
+from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
 
 from nicegui import ui
@@ -15,9 +16,21 @@ def test_entering_color(screen: Screen):
 
 
 def test_picking_color(screen: Screen):
-    ui.color_input(label='Color', on_change=lambda e: ui.label(f'content: {e.value}'))
+    ui.color_input(label='Color', on_change=lambda e: output.set_text(e.value))
+    output = ui.label()
 
     screen.open('/')
-    picker = screen.click('colorize')
-    screen.click_at_position(picker, x=40, y=120)
-    screen.should_contain('content: #de8383')
+    screen.click('colorize')
+    screen.click_at_position(screen.find('HEX'), x=0, y=60)
+    content = screen.selenium.find_element(By.CLASS_NAME, 'q-color-picker__header-content')
+    assert content.value_of_css_property('background-color') in {'rgba(245, 186, 186, 1)', 'rgba(245, 184, 184, 1)'}
+    assert output.text in {'#f5baba', '#f5b8b8'}
+
+    screen.type(Keys.ESCAPE)
+    screen.wait(0.5)
+    screen.should_not_contain('HEX')
+
+    screen.click('colorize')
+    content = screen.selenium.find_element(By.CLASS_NAME, 'q-color-picker__header-content')
+    assert content.value_of_css_property('background-color') in {'rgba(245, 186, 186, 1)', 'rgba(245, 184, 184, 1)'}
+    assert output.text in {'#f5baba', '#f5b8b8'}

+ 1 - 0
tests/test_input.py

@@ -64,4 +64,5 @@ def test_input_validation(screen: Screen):
     screen.should_contain('Too short')
 
     element.send_keys(' Doe')
+    screen.wait(0.5)
     screen.should_not_contain('Too short')

+ 10 - 0
tests/test_markdown.py

@@ -43,3 +43,13 @@ graph TD;
     screen.should_contain('New')
     assert screen.find('Node_C').get_attribute('class') == 'nodeLabel'
     screen.should_not_contain('Node_A')
+
+
+def test_strip_indentation(screen: Screen):
+    ui.markdown('''
+        **This is Markdown.**
+    ''')
+
+    screen.open('/')
+    screen.should_contain('This is Markdown.')
+    screen.should_not_contain('**This is Markdown.**')  # NOTE: '**' are translated to formatting and not visible

+ 33 - 0
tests/test_select.py

@@ -0,0 +1,33 @@
+from nicegui import ui
+
+from .screen import Screen
+
+
+def test_select(screen: Screen):
+    ui.select(['A', 'B', 'C'], value='A')
+
+    screen.open('/')
+    screen.should_contain('A')
+    screen.should_not_contain('B')
+    screen.should_not_contain('C')
+
+    screen.click('A')  # open the dropdown
+    screen.click('B')  # close the dropdown
+    screen.wait(0.5)
+    screen.should_not_contain('A')
+    screen.should_contain('B')
+    screen.should_not_contain('C')
+
+
+def test_select_with_input(screen: Screen):
+    ui.select(['A', 'AB', 'XYZ'], with_input=True)
+
+    screen.open('/')
+    screen.find_by_tag('input').click()
+    screen.should_contain('XYZ')
+
+    screen.find_by_tag('input').send_keys('A')
+    screen.wait(0.5)
+    screen.should_contain('A')
+    screen.should_contain('AB')
+    screen.should_not_contain('XYZ')

+ 22 - 7
tests/test_table.py

@@ -4,12 +4,14 @@ from nicegui import ui
 
 from .screen import Screen
 
-columns = [
+
+def columns(): return [
     {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True},
     {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
 ]
 
-rows = [
+
+def rows(): return [
     {'id': 0, 'name': 'Alice', 'age': 18},
     {'id': 1, 'name': 'Bob', 'age': 21},
     {'id': 2, 'name': 'Lionel', 'age': 19},
@@ -17,7 +19,7 @@ rows = [
 
 
 def test_table(screen: Screen):
-    ui.table(title='My Team', columns=columns, rows=rows)
+    ui.table(title='My Team', columns=columns(), rows=rows())
 
     screen.open('/')
     screen.should_contain('My Team')
@@ -28,7 +30,7 @@ def test_table(screen: Screen):
 
 
 def test_pagination(screen: Screen):
-    ui.table(columns=columns, rows=rows, pagination=2)
+    ui.table(columns=columns(), rows=rows(), pagination=2)
 
     screen.open('/')
     screen.should_contain('Alice')
@@ -38,7 +40,7 @@ def test_pagination(screen: Screen):
 
 
 def test_filter(screen: Screen):
-    table = ui.table(columns=columns, rows=rows)
+    table = ui.table(columns=columns(), rows=rows())
     ui.input('Search by name').bind_value(table, 'filter')
 
     screen.open('/')
@@ -54,7 +56,7 @@ def test_filter(screen: Screen):
 
 
 def test_add_remove(screen: Screen):
-    table = ui.table(columns=columns, rows=rows)
+    table = ui.table(columns=columns(), rows=rows())
     ui.button('Add', on_click=lambda: table.add_rows({'id': 3, 'name': 'Carol', 'age': 32}))
     ui.button('Remove', on_click=lambda: table.remove_rows(table.rows[0]))
 
@@ -68,7 +70,7 @@ def test_add_remove(screen: Screen):
 
 
 def test_slots(screen: Screen):
-    with ui.table(columns=columns, rows=rows) as table:
+    with ui.table(columns=columns(), rows=rows()) as table:
         with table.add_slot('top-row'):
             with table.row():
                 with table.cell():
@@ -87,3 +89,16 @@ def test_slots(screen: Screen):
     screen.should_not_contain('Alice')
     screen.should_contain('overridden')
     screen.should_contain('21')
+
+
+def test_single_selection(screen: Screen):
+    ui.table(columns=columns(), rows=rows(), selection='single')
+
+    screen.open('/')
+    screen.find('Alice').find_element(By.XPATH, 'preceding-sibling::td').click()
+    screen.wait(0.5)
+    screen.should_contain('1 record selected.')
+
+    screen.find('Bob').find_element(By.XPATH, 'preceding-sibling::td').click()
+    screen.wait(0.5)
+    screen.should_contain('1 record selected.')

+ 100 - 0
website/demo.py

@@ -0,0 +1,100 @@
+import inspect
+from typing import Callable, Optional
+
+import isort
+
+from nicegui import ui
+
+from .intersection_observer import IntersectionObserver as intersection_observer
+
+PYTHON_BGCOLOR = '#00000010'
+PYTHON_COLOR = '#eef5fb'
+BASH_BGCOLOR = '#00000010'
+BASH_COLOR = '#e8e8e8'
+BROWSER_BGCOLOR = '#00000010'
+BROWSER_COLOR = '#ffffff'
+
+
+def remove_prefix(text: str, prefix: str) -> str:
+    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'):
+                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
+
+
+def _window_header(bgcolor: str) -> ui.row():
+    return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
+
+
+def _dots() -> None:
+    with ui.row().classes('gap-1 relative left-[1px] top-[1px]'):
+        ui.icon('circle').classes('text-[13px] text-red-400')
+        ui.icon('circle').classes('text-[13px] text-yellow-400')
+        ui.icon('circle').classes('text-[13px] text-green-400')
+
+
+def _title(title: str) -> None:
+    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:
+    with ui.row().classes('gap-0'):
+        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(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] 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]')
+
+
+def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
+    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)'):
+        with _window_header(bgcolor):
+            _dots()
+            if title:
+                _title(title)
+            if tab:
+                _tab(tab, color, bgcolor)
+        return ui.column().classes('w-full h-full overflow-auto')
+
+
+def python_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
+    return window(PYTHON_COLOR, PYTHON_BGCOLOR, title=title or 'main.py', classes=classes).classes('p-2 python-window')
+
+
+def bash_window(*, classes: str = '') -> ui.card:
+    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:
+    return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')

+ 656 - 0
website/documentation.py

@@ -0,0 +1,656 @@
+import uuid
+
+from nicegui import app, ui
+
+from .demo import bash_window, python_window
+from .documentation_tools import element_demo, heading, intro_demo, load_demo, subheading, text_demo
+
+CONSTANT_UUID = str(uuid.uuid4())
+
+
+def create_intro() -> None:
+    @intro_demo('Styling',
+                'While having reasonable defaults, you can still modify the look of your app with CSS as well as Tailwind and Quasar classes.')
+    def formatting_demo():
+        ui.icon('thumb_up')
+        ui.markdown('This is **Markdown**.')
+        ui.html('This is <strong>HTML</strong>.')
+        with ui.row():
+            ui.label('CSS').style('color: #888; font-weight: bold')
+            ui.label('Tailwind').classes('font-serif')
+            ui.label('Quasar').classes('q-ml-xl')
+        ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
+
+    @intro_demo('Common UI Elements',
+                'NiceGUI comes with a collection of commonly used UI elements.')
+    def common_elements_demo():
+        from nicegui.events import ValueChangeEventArguments
+
+        def show(event: ValueChangeEventArguments):
+            name = type(event.sender).__name__
+            ui.notify(f'{name}: {event.value}')
+
+        ui.button('Button', on_click=lambda: ui.notify('Click'))
+        with ui.row():
+            ui.checkbox('Checkbox', on_change=show)
+            ui.switch('Switch', on_change=show)
+        ui.radio(['A', 'B', 'C'], value='A', on_change=show).props('inline')
+        with ui.row():
+            ui.input('Text input', on_change=show)
+            ui.select(['One', 'Two'], value='One', on_change=show)
+        ui.link('And many more...', '/documentation').classes('mt-8')
+
+    @intro_demo('Value Binding',
+                'Binding values between UI elements and data models is built into NiceGUI.')
+    def binding_demo():
+        class Demo:
+            def __init__(self):
+                self.number = 1
+
+        demo = Demo()
+        v = ui.checkbox('visible', value=True)
+        with ui.column().bind_visibility_from(v, 'value'):
+            ui.slider(min=1, max=3).bind_value(demo, 'number')
+            ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
+            ui.number().bind_value(demo, 'number')
+
+
+def create_full(menu: ui.element) -> None:
+    heading('Basic Elements')
+    load_demo(ui.label)
+    load_demo(ui.icon)
+    load_demo(ui.avatar)
+    load_demo(ui.link)
+    load_demo(ui.button)
+    load_demo(ui.badge)
+    load_demo(ui.toggle)
+    load_demo(ui.radio)
+    load_demo(ui.select)
+    load_demo(ui.checkbox)
+    load_demo(ui.switch)
+    load_demo(ui.slider)
+    load_demo(ui.joystick)
+    load_demo(ui.input)
+    load_demo(ui.textarea)
+    load_demo(ui.number)
+    load_demo(ui.knob)
+    load_demo(ui.color_input)
+    load_demo(ui.color_picker)
+    load_demo(ui.date)
+    load_demo(ui.time)
+    load_demo(ui.upload)
+    load_demo(ui.element)
+
+    heading('Markdown and HTML')
+
+    load_demo(ui.markdown)
+    load_demo(ui.mermaid)
+    load_demo(ui.html)
+
+    @text_demo('SVG',
+               'You can add Scalable Vector Graphics using the `ui.html` element.')
+    def svg_demo():
+        content = '''
+            <svg viewBox="0 0 200 200" width="100" height="100" 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.html(content)
+
+    heading('Images, Audio and Video')
+
+    load_demo(ui.image)
+
+    @text_demo('Captions and Overlays', '''
+        By nesting elements inside a `ui.image` you can create augmentations.
+
+        Use [Quasar classes](https://quasar.dev/vue-components/img) for positioning and styling captions.
+        To overlay an SVG, make the `viewBox` exactly the size of the image and provide `100%` width/height to match the actual rendered size.
+    ''')
+    def captions_and_overlays_demo():
+        with ui.image('https://picsum.photos/id/29/640/360'):
+            ui.label('Nice!').classes('absolute-bottom text-subtitle2 text-center')
+
+        with ui.image('https://cdn.stocksnap.io/img-thumbs/960w/airplane-sky_DYPWDEEILG.jpg'):
+            ui.html('''
+                <svg viewBox="0 0 960 638" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
+                <circle cx="445" cy="300" r="100" fill="none" stroke="red" stroke-width="20" />
+                </svg>
+            ''').classes('bg-transparent')
+
+    load_demo(ui.interactive_image)
+    load_demo(ui.audio)
+    load_demo(ui.video)
+
+    heading('Data Elements')
+
+    load_demo(ui.table)
+    load_demo(ui.aggrid)
+    load_demo(ui.chart)
+    load_demo(ui.pyplot)
+    load_demo(ui.line_plot)
+    load_demo(ui.plotly)
+    load_demo(ui.linear_progress)
+    load_demo(ui.circular_progress)
+    load_demo(ui.spinner)
+    load_demo(ui.scene)
+    load_demo(ui.tree)
+    load_demo(ui.log)
+
+    heading('Layout')
+
+    load_demo(ui.card)
+    load_demo(ui.column)
+    load_demo(ui.row)
+
+    @text_demo('Clear Containers', '''
+        To remove all elements from a row, column or card container, use the `clear()` method.
+
+        Alternatively, you can remove individual elements with `remove(element)`, where `element` is an Element or an index.
+    ''')
+    def clear_containers_demo():
+        container = ui.row()
+
+        def add_face():
+            with container:
+                ui.icon('face')
+        add_face()
+
+        ui.button('Add', on_click=add_face)
+        ui.button('Remove', on_click=lambda: container.remove(0))
+        ui.button('Clear', on_click=container.clear)
+
+    load_demo(ui.expansion)
+
+    @text_demo('Tabs', '''
+        The elements `ui.tabs`, `ui.tab`, `ui.tab_panels`, and `ui.tab_panel` resemble
+        [Quasar's tabs](https://quasar.dev/vue-components/tabs)
+        and [tab panels](https://quasar.dev/vue-components/tab-panels) API.
+
+        `ui.tabs` creates a container for the tabs. This could be placed in a `ui.header` for example.
+        `ui.tab_panels` creates a container for the tab panels with the actual content.
+    ''')
+    def tabs_demo():
+        with ui.tabs() as tabs:
+            ui.tab('Home', icon='home')
+            ui.tab('About', icon='info')
+
+        with ui.tab_panels(tabs, value='Home'):
+            with ui.tab_panel('Home'):
+                ui.label('This is the first tab')
+            with ui.tab_panel('About'):
+                ui.label('This is the second tab')
+
+    load_demo(ui.menu)
+
+    @text_demo('Tooltips', '''
+        Simply call the `tooltip(text:str)` method on UI elements to provide a tooltip.
+
+        For more artistic control you can nest tooltip elements and apply props, classes and styles.
+    ''')
+    def tooltips_demo():
+        ui.label('Tooltips...').tooltip('...are shown on mouse over')
+        with ui.button().props('icon=thumb_up'):
+            ui.tooltip('I like this').classes('bg-green')
+
+    load_demo(ui.notify)
+    load_demo(ui.dialog)
+
+    @text_demo('Awaitable dialog', '''
+        Dialogs can be awaited.
+        Use the `submit` method to close the dialog and return a result.
+        Canceling the dialog by clicking in the background or pressing the escape key yields `None`.
+    ''')
+    def async_dialog_demo():
+        with ui.dialog() as dialog, ui.card():
+            ui.label('Are you sure?')
+            with ui.row():
+                ui.button('Yes', on_click=lambda: dialog.submit('Yes'))
+                ui.button('No', on_click=lambda: dialog.submit('No'))
+
+        async def show():
+            result = await dialog
+            ui.notify(f'You chose {result}')
+
+        ui.button('Await a dialog', on_click=show)
+
+    heading('Appearance')
+
+    @text_demo('Styling', '''
+        NiceGUI uses the [Quasar Framework](https://quasar.dev/) version 1.0 and hence has its full design power.
+        Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
+        Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
+        You can also apply [Tailwind](https://tailwindcss.com/) utility classes with the `classes` method.
+
+        If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.
+
+        All three functions also provide `remove` and `replace` parameters in case the predefined look is not wanted in a particular styling.
+    ''')
+    def design_demo():
+        ui.radio(['x', 'y', 'z'], value='x').props('inline color=green')
+        ui.button().props('icon=touch_app outline round').classes('shadow-lg')
+        ui.label('Stylish!').style('color: #6E93D6; font-size: 200%; font-weight: 300')
+
+    load_demo(ui.colors)
+
+    heading('Action')
+
+    load_demo(ui.timer)
+    load_demo(ui.keyboard)
+
+    @text_demo('Bindings', '''
+        NiceGUI is able to directly bind UI elements to models.
+        Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
+
+        Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
+        To define a one-way binding use the `_from` and `_to` variants of these methods.
+        Just pass a property of the model as parameter to these methods to create the binding.
+    ''')
+    def bindings_demo():
+        class Demo:
+            def __init__(self):
+                self.number = 1
+
+        demo = Demo()
+        v = ui.checkbox('visible', value=True)
+        with ui.column().bind_visibility_from(v, 'value'):
+            ui.slider(min=1, max=3).bind_value(demo, 'number')
+            ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
+            ui.number().bind_value(demo, 'number')
+
+    @text_demo('UI Updates', '''
+        NiceGUI tries to automatically synchronize the state of UI elements with the client, e.g. when a label text, an input value or style/classes/props of an element have changed.
+        In other cases, you can explicitly call `element.update()` or `ui.update(*elements)` to update.
+        The demo code shows both methods for a `ui.chart`, where it is difficult to automatically detect changes in the `options` dictionary.
+    ''')
+    def ui_updates_demo():
+        from random import randint
+
+        chart = ui.chart({'title': False, 'series': [{'data': [1, 2]}]}).classes('w-full h-64')
+
+        def add():
+            chart.options['series'][0]['data'].append(randint(0, 100))
+            chart.update()
+
+        def clear():
+            chart.options['series'][0]['data'].clear()
+            ui.update(chart)
+
+        with ui.row():
+            ui.button('Add', on_click=add)
+            ui.button('Clear', on_click=clear)
+
+    @text_demo('Async event handlers', '''
+        Most elements also support asynchronous event handlers.
+
+        Note: You can also pass a `functools.partial` into the `on_click` property to wrap async functions with parameters.
+    ''')
+    def async_handlers_demo():
+        import asyncio
+
+        async def async_task():
+            ui.notify('Asynchronous task started')
+            await asyncio.sleep(5)
+            ui.notify('Asynchronous task finished')
+
+        ui.button('start async task', on_click=async_task)
+
+    heading('Pages')
+
+    load_demo(ui.page)
+
+    @text_demo('Auto-index page', '''
+        Pages created with the `@ui.page` decorator are "private".
+        Their content is re-created for each client.
+        Thus, in the demo to the right, the displayed ID on the private page changes when the browser reloads the page.
+
+        UI elements that are not wrapped in a decorated page function are placed on an automatically generated index page at route "/".
+        This auto-index page is created once on startup and *shared* across all clients that might connect.
+        Thus, each connected client will see the *same* elements.
+        In the demo to the right, the displayed ID on the auto-index page remains constant when the browser reloads the page.
+    ''')
+    def auto_index_page():
+        from uuid import uuid4
+
+        @ui.page('/private_page')
+        async def private_page():
+            ui.label(f'private page with ID {uuid4()}')
+
+        # ui.label(f'shared auto-index page with ID {uuid4()}')
+        # ui.link('private page', private_page)
+        # END OF DEMO
+        ui.label(f'shared auto-index page with ID {CONSTANT_UUID}')
+        ui.link('private page', private_page)
+
+    @text_demo('Pages with Path Parameters', '''
+        Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
+        If type-annotated, they are automatically converted to bool, int, float and complex values.
+        If the page function expects a `request` argument, the request object is automatically provided.
+        The `client` argument provides access to the websocket connection, layout, etc.
+    ''')
+    def page_with_path_parameters_demo():
+        @ui.page('/repeat/{word}/{count}')
+        def page(word: str, count: int):
+            ui.label(word * count)
+
+        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
+
+    @text_demo('Wait for Client Connection', '''
+        To wait for a client connection, you can add a `client` argument to the decorated page function
+        and await `client.connected()`.
+        All code below that statement is executed after the websocket connection between server and client has been established.
+
+        For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
+        Also it is possible to do async stuff while the user already sees some content.
+    ''')
+    def wait_for_connected_demo():
+        import asyncio
+
+        from nicegui import Client
+
+        @ui.page('/wait_for_connection')
+        async def wait_for_connection(client: Client):
+            ui.label('This text is displayed immediately.')
+            await client.connected()
+            await asyncio.sleep(2)
+            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
+            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
+
+        ui.link('wait for connection', wait_for_connection)
+
+    @text_demo('Page Layout', '''
+        With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
+        The `fixed` argument controls whether the element should scroll or stay fixed on the screen.
+        The `top_corner` and `bottom_corner` arguments indicate whether a drawer should expand to the top or bottom of the page.
+        See <https://quasar.dev/layout/header-and-footer> and <https://quasar.dev/layout/drawer> for more information about possible props.
+        With `ui.page_sticky` you can place an element "sticky" on the screen.
+        See <https://quasar.dev/layout/page-sticky> for more information.
+    ''')
+    def page_layout_demo():
+        @ui.page('/page_layout')
+        async def page_layout():
+            ui.label('CONTENT')
+            [ui.label(f'Line {i}') for i in range(100)]
+            with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
+                ui.label('HEADER')
+                ui.button(on_click=lambda: right_drawer.toggle()).props('flat color=white icon=menu')
+            with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4'):
+                ui.label('LEFT DRAWER')
+            with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as right_drawer:
+                ui.label('RIGHT DRAWER')
+            with ui.footer().style('background-color: #3874c8'):
+                ui.label('FOOTER')
+
+        ui.link('show page with fancy layout', page_layout)
+
+    load_demo(ui.open)
+
+    @text_demo('Sessions', '''
+        The optional `request` argument provides insights about the client's URL parameters etc.
+        It also enables you to identify sessions using a [session middleware](https://www.starlette.io/middleware/#sessionmiddleware).
+    ''')
+    def sessions_demo():
+        import uuid
+        from collections import Counter
+        from datetime import datetime
+
+        from starlette.middleware.sessions import SessionMiddleware
+        from starlette.requests import Request
+
+        from nicegui import app
+
+        # app.add_middleware(SessionMiddleware, secret_key='some_random_string')
+
+        counter = Counter()
+        start = datetime.now().strftime('%H:%M, %d %B %Y')
+
+        @ui.page('/session_demo')
+        def session_demo(request: Request):
+            if 'id' not in request.session:
+                request.session['id'] = str(uuid.uuid4())
+            counter[request.session['id']] += 1
+            ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
+
+        ui.link('Visit session demo', session_demo)
+
+    load_demo(ui.run_javascript)
+
+    heading('Routes')
+
+    @element_demo(app.add_static_files)
+    def add_static_files_demo():
+        from nicegui import app
+
+        app.add_static_files('/examples', 'examples')
+        ui.label('Some NiceGUI Examples').classes('text-h5')
+        ui.link('AI interface', '/examples/ai_interface/main.py')
+        ui.link('Custom FastAPI app', '/examples/fastapi/main.py')
+        ui.link('Authentication', '/examples/authentication/main.py')
+
+    @text_demo('API Responses', '''
+        NiceGUI is based on [FastAPI](https://fastapi.tiangolo.com/).
+        This means you can use all of FastAPI's features.
+        For example, you can implement a RESTful API in addition to your graphical user interface.
+        You simply import the `app` object from `nicegui`.
+        Or you can run NiceGUI on top of your own FastAPI app by using `ui.run_with(app)` instead of starting a server automatically with `ui.run()`.
+
+        You can also return any other FastAPI response object inside a page function.
+        For example, you can return a `RedirectResponse` to redirect the user to another page if certain conditions are met.
+        This is used in our [authentication demo](https://github.com/zauberzeug/nicegui/tree/main/examples/authentication/main.py).
+    ''')
+    def fastapi_demo():
+        import random
+
+        from nicegui import app
+
+        @app.get('/random/{max}')
+        def generate_random_number(max: int):
+            return {'min': 0, 'max': max, 'value': random.randint(0, max)}
+
+        max = ui.number('max', value=100)
+        ui.button('generate random number', on_click=lambda: ui.open(f'/random/{max.value:.0f}'))
+
+    heading('Lifecycle')
+
+    @text_demo('Events', '''
+        You can register coroutines or functions to be called for the following events:
+
+        - `app.on_startup`: called when NiceGUI is started or restarted
+        - `app.on_shutdown`: called when NiceGUI is shut down or restarted
+        - `app.on_connect`: called for each client which connects (optional argument: nicegui.Client)
+        - `app.on_disconnect`: called for each client which disconnects (optional argument: nicegui.Client)
+        - `app.on_exception`: called when an exception occurs (optional argument: exception)
+
+        When NiceGUI is shut down or restarted, all tasks still in execution will be automatically canceled.
+    ''')
+    def lifecycle_demo():
+        from datetime import datetime
+
+        from nicegui import app
+
+        # dt = datetime.now()
+
+        def handle_connection():
+            global dt
+            dt = datetime.now()
+        app.on_connect(handle_connection)
+
+        label = ui.label()
+        ui.timer(1, lambda: label.set_text(f'Last new connection: {dt:%H:%M:%S}'))
+        # END OF DEMO
+        global dt
+        dt = datetime.now()
+
+    @element_demo(app.shutdown)
+    def shutdown_demo():
+        from nicegui import app
+
+        # ui.button('shutdown', on_click=app.shutdown)
+        #
+        # ui.run(reload=False)
+        # END OF DEMO
+        ui.button('shutdown', on_click=lambda: ui.notify(
+            'Nah. We do not actually shutdown the documentation server. Try it in your own app!'))
+
+    heading('NiceGUI Fundamentals')
+
+    @text_demo('Auto-context', '''
+        In order to allow writing intuitive UI descriptions, NiceGUI automatically tracks the context in which elements are created.
+        This means that there is no explicit `parent` parameter.
+        Instead the parent context is defined using a `with` statement.
+        It is also passed to event handlers and timers.
+
+        In the demo, the label "Card content" is added to the card.
+        And because the `ui.button` is also added to the card, the label "Click!" will also be created in this context.
+        The label "Tick!", which is added once after one second, is also added to the card.
+
+        This design decision allows for easily creating modular components that keep working after moving them around in the UI.
+        For example, you can move label and button somewhere else, maybe wrap them in another container, and the code will still work.
+    ''')
+    def auto_context_demo():
+        with ui.card():
+            ui.label('Card content')
+            ui.button('Add label', on_click=lambda: ui.label('Click!'))
+            ui.timer(1.0, lambda: ui.label('Tick!'), once=True)
+
+    @text_demo('Generic Events', '''
+        Most UI elements come with predefined events.
+        For example, a `ui.button` like "A" in the demo has an `on_click` parameter that expects a coroutine or function.
+        But you can also use the `on` method to register a generic event handler like for "B".
+        This allows you to register handlers for any event that is supported by JavaScript and Quasar.
+
+        For example, you can register a handler for the `mousemove` event like for "C", even though there is no `on_mousemove` parameter for `ui.button`.
+        Some events, like `mousemove`, are fired very often.
+        To avoid performance issues, you can use the `throttle` parameter to only call the handler every `throttle` seconds ("D").
+
+        The generic event handler can be synchronous or asynchronous and optionally takes an event dictionary as argument ("E").
+        You can also specify which attributes of the JavaScript or Quasar event should be passed to the handler ("F").
+        This can reduce the amount of data that needs to be transferred between the server and the client.
+
+        You can also include [key modifiers](https://vuejs.org/guide/essentials/event-handling.html#key-modifiers) ("G"),
+        modifier combinations ("H"),
+        and [event modifiers](https://vuejs.org/guide/essentials/event-handling.html#mouse-button-modifiers) ("I").
+    ''')
+    def generic_events_demo():
+        with ui.row():
+            ui.button('A', on_click=lambda: ui.notify('You clicked the button A.'))
+            ui.button('B').on('click', lambda: ui.notify('You clicked the button B.'))
+        with ui.row():
+            ui.button('C').on('mousemove', lambda: ui.notify('You moved on button C.'))
+            ui.button('D').on('mousemove', lambda: ui.notify('You moved on button D.'), throttle=0.5)
+        with ui.row():
+            ui.button('E').on('mousedown', lambda e: ui.notify(str(e)))
+            ui.button('F').on('mousedown', lambda e: ui.notify(str(e)), ['ctrlKey', 'shiftKey'])
+        with ui.row():
+            ui.input('G').classes('w-12').on('keydown.space', lambda: ui.notify('You pressed space.'))
+            ui.input('H').classes('w-12').on('keydown.y.shift', lambda: ui.notify('You pressed Shift+Y'))
+            ui.input('I').classes('w-12').on('keydown.once', lambda: ui.notify('You started typing.'))
+    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')
+
+    @text_demo('Environment Variables', '''
+        You can set the following environment variables to configure NiceGUI:
+
+        - `MATPLOTLIB` (default: true) can be set to `false` to avoid the potentially costly import of Matplotlib. This will make `ui.pyplot` and `ui.line_plot` unavailable.
+        - `MARKDOWN_CONTENT_CACHE_SIZE` (default: 1000): The maximum number of Markdown content snippets that are cached in memory.
+    ''')
+    def env_var_demo():
+        from nicegui.elements import markdown
+
+        ui.label(f'Markdown content cache size is {markdown.prepare_content.cache_info().maxsize}')
+
+    heading('Deployment')
+
+    subheading('Server Hosting')
+
+    ui.markdown('''
+        To deploy your NiceGUI app on a server, you will need to execute your `main.py` (or whichever file contains your `ui.run(...)`) on your cloud infrastructure.
+        You can, for example, just install the [NiceGUI python package via pip](https://pypi.org/project/nicegui/) and use systemd or similar service to start the main script.
+        In most cases, you will set the port to 80 (or 443 if you want to use HTTPS) with the `ui.run` command to make it easily accessible from the outside.
+
+        A convenient alternative is the use of our [pre-built multi-arch Docker image](https://hub.docker.com/r/zauberzeug/nicegui) which contains all necessary dependencies.
+        With this command you can launch the script `main.py` in the current directory on the public port 80:
+    ''')
+    with bash_window(classes='max-w-lg w-full h-52'):
+        ui.markdown('''
+            ```bash
+            docker run -p 80:8080 -v $(pwd)/:/app/ \\
+                -d --restart always zauberzeug/nicegui:latest
+            ```
+        ''')
+    ui.markdown('''
+        The demo assumes `main.py` uses the port 8080 in the `ui.run` command (which is the default).
+        The `-d` tells docker to run in background and `--restart always` makes sure the container is restarted if the app crashes or the server reboots.
+        Of course this can also be written in a Docker compose file:
+    ''')
+    with python_window('docker-compose.yml', classes='max-w-lg w-full h-52'):
+        ui.markdown('''
+            ```yaml
+            app:
+                image: zauberzeug/nicegui:latest
+                restart: always
+                ports:
+                    - 80:8080
+                volumes:
+                    - ./:/app/
+            ```
+        ''')
+
+    ui.markdown('''
+        You can provide SSL certificates directly using [FastAPI](https://fastapi.tiangolo.com/deployment/https/).
+        In production we also like using reverse proxies like [Traefik](https://doc.traefik.io/traefik/) or [NGINX](https://www.nginx.com/) to handle these details for us.
+        See our [docker-compose.yml](https://github.com/zauberzeug/nicegui/blob/main/docker-compose.yml) as an example.
+
+        You may also have a look at [our demo for using a custom FastAPI app](https://github.com/zauberzeug/nicegui/tree/main/examples/fastapi).
+        This will allow you to do very flexible deployments as described in the [FastAPI documentation](https://fastapi.tiangolo.com/deployment/).
+        Note that there are additional steps required to allow multiple workers.
+    ''')
+
+    subheading('Package for Installation')
+
+    ui.markdown('''
+        NiceGUI apps can also be bundled into an executable with [PyInstaller](https://www.pyinstaller.org/).
+        This allows you to distribute your app as a single file that can be executed on any computer.
+
+        Just take care your `ui.run` command does not use the `reload` argument.
+        Running the `build.py` below will create an executable `myapp` in the `dist` folder:
+    ''')
+
+    with ui.row().classes('w-full items-stretch'):
+        with python_window(classes='max-w-lg w-full'):
+            ui.markdown('''
+                ```python
+                from nicegui import ui
+
+                ui.label('Hello from PyInstaller')
+
+                ui.run(reload=False)
+                ```
+            ''')
+        with python_window('build.py', classes='max-w-lg w-full'):
+            ui.markdown('''
+                ```python
+                import os
+                import subprocess
+                from pathlib import Path
+                import nicegui
+
+                cmd = [
+                    'pyinstaller',
+                    'main.py', # your main file with ui.run()
+                    '--name', 'myapp', # name of your app
+                    '--onefile',
+                    '--add-data', f'{Path(nicegui.__file__).parent}{os.pathsep}nicegui'       
+                ]
+                subprocess.call(cmd)
+                ```
+            ''')
+
+    ui.element('div').classes('h-32')

+ 188 - 0
website/documentation_tools.py

@@ -0,0 +1,188 @@
+import importlib
+import inspect
+import re
+from typing import Callable, Optional, Union
+
+import docutils.core
+
+from nicegui import globals, ui
+from nicegui.binding import BindableProperty
+from nicegui.elements.markdown import apply_tailwind, remove_indentation
+
+from .demo import demo
+
+SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
+
+
+def pascal_to_snake(name: str) -> str:
+    return re.sub(r'(?<!^)(?=[A-Z])', '_', name).lower()
+
+
+def create_anchor_name(text: str) -> str:
+    return SPECIAL_CHARACTERS.sub('_', text).lower()
+
+
+def get_menu() -> ui.left_drawer:
+    return [element for element in globals.get_client().elements.values() if isinstance(element, ui.left_drawer)][0]
+
+
+def heading(text: str, *, make_menu_entry: bool = True) -> None:
+    ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
+    if make_menu_entry:
+        with get_menu():
+            ui.label(text).classes('font-bold mt-4')
+
+
+def subheading(text: str, *, make_menu_entry: bool = True, more_link: Optional[str] = None) -> None:
+    name = create_anchor_name(text)
+    ui.html(f'<div id="{name}"></div>').style('position: relative; top: -90px')
+    with ui.row().classes('gap-2 items-center relative'):
+        if more_link:
+            with ui.link(text, f'documentation/{more_link}').classes('text-2xl'):
+                ui.icon('open_in_new', size='0.75em').classes('mb-1 ml-2')
+        else:
+            ui.label(text).classes('text-2xl')
+        with ui.link(target=f'#{name}').classes('absolute').style('transform: translateX(-150%)'):
+            ui.icon('link', size='sm').classes('opacity-10 hover:opacity-80')
+    if make_menu_entry:
+        with get_menu() as menu:
+            async def click():
+                if await ui.run_javascript(f'!!document.querySelector("div.q-drawer__backdrop")'):
+                    menu.hide()
+                    ui.open(f'#{name}')
+            ui.link(text, target=f'#{name}').props('data-close-overlay').on('click', click)
+
+
+def render_docstring(doc: str, with_params: bool = True) -> ui.html:
+    doc = remove_indentation(doc)
+    doc = doc.replace('param ', '')
+    html = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
+    html = apply_tailwind(html)
+    if not with_params:
+        html = re.sub(r'<dl class=".* simple">.*?</dl>', '', html, flags=re.DOTALL)
+    return ui.html(html).classes('documentation bold-links arrow-links')
+
+
+class text_demo:
+
+    def __init__(self, title: str, explanation: str) -> None:
+        self.title = title
+        self.explanation = explanation
+        self.make_menu_entry = True
+
+    def __call__(self, f: Callable) -> Callable:
+        subheading(self.title, make_menu_entry=self.make_menu_entry)
+        ui.markdown(self.explanation).classes('bold-links arrow-links')
+        return demo()(f)
+
+
+class intro_demo(text_demo):
+
+    def __init__(self, title: str, explanation: str) -> None:
+        super().__init__(title, explanation)
+        self.make_menu_entry = False
+
+
+class element_demo:
+
+    def __init__(self, element_class: Union[Callable, type], browser_title: Optional[str] = None) -> None:
+        self.element_class = element_class
+        self.browser_title = browser_title
+
+    def __call__(self, f: Callable, *, more_link: Optional[str] = None) -> Callable:
+        doc = self.element_class.__doc__ or self.element_class.__init__.__doc__
+        title, documentation = doc.split('\n', 1)
+        with ui.column().classes('w-full mb-8 gap-2'):
+            subheading(title, more_link=more_link)
+            render_docstring(documentation, with_params=more_link is None)
+            return demo(browser_title=self.browser_title)(f)
+
+
+def load_demo(element_class: type) -> None:
+    name = pascal_to_snake(element_class.__name__)
+    try:
+        module = importlib.import_module(f'website.more_documentation.{name}_documentation')
+    except ModuleNotFoundError:
+        module = importlib.import_module(f'website.more_documentation.{name.replace("_", "")}_documentation')
+    element_demo(element_class)(getattr(module, 'main_demo'), more_link=name)
+
+
+def is_method_or_property(cls: type, attribute_name: str) -> bool:
+    attribute = cls.__dict__.get(attribute_name, None)
+    return (
+        inspect.isfunction(attribute) or
+        inspect.ismethod(attribute) or
+        isinstance(attribute, property) or
+        isinstance(attribute, BindableProperty)
+    )
+
+
+def generate_class_doc(class_obj: type) -> None:
+    mro = [base for base in class_obj.__mro__ if base.__module__.startswith('nicegui.')]
+    ancestors = mro[1:]
+    attributes = {}
+    for base in reversed(mro):
+        for name in dir(base):
+            if not name.startswith('_') and is_method_or_property(base, name):
+                attributes[name] = getattr(base, name, None)
+    properties = {name: attribute for name, attribute in attributes.items() if not callable(attribute)}
+    methods = {name: attribute for name, attribute in attributes.items() if callable(attribute)}
+
+    if properties:
+        subheading('Properties')
+        with ui.column().classes('gap-2'):
+            for name, property in sorted(properties.items()):
+                ui.markdown(f'**`{name}`**`{generate_property_signature_description(property)}`')
+                if property.__doc__:
+                    render_docstring(property.__doc__).classes('ml-8')
+    if methods:
+        subheading('Methods')
+        with ui.column().classes('gap-2'):
+            for name, method in sorted(methods.items()):
+                ui.markdown(f'**`{name}`**`{generate_method_signature_description(method)}`')
+                if method.__doc__:
+                    render_docstring(method.__doc__).classes('ml-8')
+    if ancestors:
+        subheading('Inherited from')
+        with ui.column().classes('gap-2'):
+            for ancestor in ancestors:
+                ui.markdown(f'- `{ancestor.__name__}`')
+
+
+def generate_method_signature_description(method: Callable) -> str:
+    param_strings = []
+    for param in inspect.signature(method).parameters.values():
+        param_string = param.name
+        if param_string == 'self':
+            continue
+        if param.annotation != inspect.Parameter.empty:
+            param_type = inspect.formatannotation(param.annotation)
+            param_string += f''': {param_type.strip("'")}'''
+        if param.default != inspect.Parameter.empty:
+            param_string += f' = [...]' if callable(param.default) else f' = {repr(param.default)}'
+        if param.kind == inspect.Parameter.VAR_POSITIONAL:
+            param_string = f'*{param_string}'
+        param_strings.append(param_string)
+    method_signature = ', '.join(param_strings)
+    description = f'({method_signature})'
+    return_annotation = inspect.signature(method).return_annotation
+    if return_annotation != inspect.Parameter.empty:
+        return_type = inspect.formatannotation(return_annotation)
+        description += f''' -> {return_type.strip("'").replace("typing_extensions.", "").replace("typing.", "")}'''
+    return description
+
+
+def generate_property_signature_description(property: Optional[property]) -> str:
+    description = ''
+    if property is None:
+        return ': BindableProperty'
+    if property.fget:
+        return_annotation = inspect.signature(property.fget).return_annotation
+        if return_annotation != inspect.Parameter.empty:
+            return_type = inspect.formatannotation(return_annotation)
+            description += f': {return_type}'
+    if property.fset:
+        description += ' (settable)'
+    if property.fdel:
+        description += ' (deletable)'
+    return description

+ 0 - 139
website/example.py

@@ -1,139 +0,0 @@
-import inspect
-import re
-from typing import Callable, Optional, Union
-
-import docutils.core
-import isort
-
-from nicegui import ui
-from nicegui.elements.markdown import apply_tailwind, prepare_content
-
-from .intersection_observer import IntersectionObserver as intersection_observer
-
-REGEX_H4 = re.compile(r'<h4.*?>(.*?)</h4>')
-SPECIAL_CHARACTERS = re.compile('[^(a-z)(A-Z)(0-9)-]')
-PYTHON_BGCOLOR = '#00000010'
-PYTHON_COLOR = '#eef5fb'
-BASH_BGCOLOR = '#00000010'
-BASH_COLOR = '#e8e8e8'
-BROWSER_BGCOLOR = '#00000010'
-BROWSER_COLOR = '#ffffff'
-
-
-def remove_prefix(text: str, prefix: str) -> str:
-    return text[len(prefix):] if text.startswith(prefix) else text
-
-
-def add_html_with_anchor_link(html: str, menu: Optional[ui.drawer]) -> str:
-    match = REGEX_H4.search(html)
-    headline = match.groups()[0].strip()
-    headline_id = SPECIAL_CHARACTERS.sub('_', headline).lower()
-    icon = '<span class="material-icons">link</span>'
-    link = f'<a href="#{headline_id}" class="hover:text-black auto-link" style="color: #ddd">{icon}</a>'
-    target = f'<div id="{headline_id}" style="position: relative; top: -90px"></div>'
-    html = html.replace('<h4', f'{target}<h4', 1)
-    html = html.replace('</h4>', f' {link}</h4>', 1)
-
-    ui.html(html).classes('documentation bold-links arrow-links')
-    if menu:
-        with menu:
-            async def click():
-                if await ui.run_javascript(f'!!document.querySelector("div.q-drawer__backdrop")'):
-                    menu.hide()
-                    ui.open(f'#{headline_id}')
-            ui.link(headline, f'#{headline_id}').props('data-close-overlay').on('click', click)
-
-
-class example:
-
-    def __init__(self,
-                 content: Union[Callable, type, str],
-                 menu: Optional[ui.drawer],
-                 browser_title: Optional[str] = None,
-                 immediate: bool = False) -> None:
-        self.content = content
-        self.menu = menu
-        self.browser_title = browser_title
-        self.immediate = immediate
-
-    def __call__(self, f: Callable) -> Callable:
-        with ui.column().classes('w-full mb-8'):
-            if isinstance(self.content, str):
-                html = prepare_content(self.content, 'fenced-code-blocks tables')
-            else:
-                doc = self.content.__doc__ or self.content.__init__.__doc__
-                html: str = docutils.core.publish_parts(doc, writer_name='html5_polyglot')['html_body']
-                html = html.replace('<p>', '<h4>', 1)
-                html = html.replace('</p>', '</h4>', 1)
-                html = html.replace('param ', '')
-                html = apply_tailwind(html)
-            add_html_with_anchor_link(html, self.menu)
-
-            with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
-                code = inspect.getsource(f).split('# END OF EXAMPLE')[0].strip().splitlines()
-                while not code[0].startswith(' ' * 8):
-                    del code[0]
-                code = ['from nicegui import ui'] + [remove_prefix(line[8:], '# ') 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]'):
-                    ui.markdown(f'```python\n{code}\n```')
-                with browser_window(self.browser_title,
-                                    classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
-                    if self.immediate:
-                        f()
-                    else:
-                        intersection_observer(on_intersection=f)
-
-        return f
-
-
-def _window_header(bgcolor: str) -> ui.row():
-    return ui.row().classes(f'w-full h-8 p-2 bg-[{bgcolor}]')
-
-
-def _dots() -> None:
-    with ui.row().classes('gap-1 relative left-[1px] top-[1px]'):
-        ui.icon('circle').classes('text-[13px] text-red-400')
-        ui.icon('circle').classes('text-[13px] text-yellow-400')
-        ui.icon('circle').classes('text-[13px] text-green-400')
-
-
-def _title(title: str) -> None:
-    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:
-    with ui.row().classes('gap-0'):
-        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(name).classes(f'text-sm text-gray-600 px-6 py-1 h-[24px] rounded-t-[6px] 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]')
-
-
-def window(color: str, bgcolor: str, *, title: str = '', tab: str = '', classes: str = '') -> ui.column:
-    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)'):
-        with _window_header(bgcolor):
-            _dots()
-            if title:
-                _title(title)
-            if tab:
-                _tab(tab, color, bgcolor)
-        return ui.column().classes('w-full h-full overflow-auto')
-
-
-def python_window(title: Optional[str] = None, *, classes: str = '') -> ui.card:
-    return window(PYTHON_COLOR, PYTHON_BGCOLOR, title=title or 'main.py', classes=classes).classes('p-2 python-window')
-
-
-def bash_window(*, classes: str = '') -> ui.card:
-    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:
-    return window(BROWSER_COLOR, BROWSER_BGCOLOR, tab=title or 'NiceGUI', classes=classes).classes('p-4 browser-window')

+ 0 - 0
website/demo_card.py → website/example_card.py


+ 0 - 0
website/more_documentation/__init__.py


+ 11 - 0
website/more_documentation/add_static_files_documentation.py

@@ -0,0 +1,11 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from nicegui import app
+
+    app.add_static_files('/examples', 'examples')
+    ui.label('Some NiceGUI Examples').classes('text-h5')
+    ui.link('AI interface', '/examples/ai_interface/main.py')
+    ui.link('Custom FastAPI app', '/examples/fastapi/main.py')
+    ui.link('Authentication', '/examples/authentication/main.py')

+ 71 - 0
website/more_documentation/aggrid_documentation.py

@@ -0,0 +1,71 @@
+from nicegui import ui
+
+from ..documentation_tools import text_demo
+
+
+def main_demo() -> None:
+    grid = ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name'},
+            {'headerName': 'Age', 'field': 'age'},
+        ],
+        'rowData': [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol', 'age': 42},
+        ],
+        'rowSelection': 'multiple',
+    }).classes('max-h-40')
+
+    def update():
+        grid.options['rowData'][0]['age'] += 1
+        grid.update()
+
+    ui.button('Update', on_click=update)
+    ui.button('Select all', on_click=lambda: grid.call_api_method('selectAll'))
+
+
+def more() -> None:
+    @text_demo('Select AG Grid Rows', '''
+        You can add checkboxes to grid cells to allow the user to select single or multiple rows.
+
+        To retrieve the currently selected rows, use the `get_selected_rows` method.
+        This method returns a list of rows as dictionaries.
+
+        If `rowSelection` is set to `'single'` or to get the first selected row,
+        you can also use the `get_selected_row` method.
+        This method returns a single row as a dictionary or `None` if no row is selected.
+
+        See the [AG Grid documentation](https://www.ag-grid.com/javascript-data-grid/row-selection/#example-single-row-selection) for more information.
+    ''')
+    def aggrid_with_selectable_rows():
+        grid = ui.aggrid({
+            'columnDefs': [
+                {'headerName': 'Name', 'field': 'name', 'checkboxSelection': True},
+                {'headerName': 'Age', 'field': 'age'},
+            ],
+            'rowData': [
+                {'name': 'Alice', 'age': 18},
+                {'name': 'Bob', 'age': 21},
+                {'name': 'Carol', 'age': 42},
+            ],
+            'rowSelection': 'multiple',
+        }).classes('max-h-40')
+
+        async def output_selected_rows():
+            rows = await grid.get_selected_rows()
+            if rows:
+                for row in rows:
+                    ui.notify(f"{row['name']}, {row['age']}")
+            else:
+                ui.notify('No rows selected.')
+
+        async def output_selected_row():
+            row = await grid.get_selected_row()
+            if row:
+                ui.notify(f"{row['name']}, {row['age']}")
+            else:
+                ui.notify('No row selected!')
+
+        ui.button('Output selected rows', on_click=output_selected_rows)
+        ui.button('Output selected row', on_click=output_selected_row)

+ 9 - 0
website/more_documentation/audio_documentation.py

@@ -0,0 +1,9 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    a = ui.audio('https://cdn.pixabay.com/download/audio/2022/02/22/audio_d1718ab41b.mp3')
+    a.on('ended', lambda _: ui.notify('Audio playback completed'))
+
+    ui.button(on_click=lambda: a.props('muted')).props('outline icon=volume_off')
+    ui.button(on_click=lambda: a.props(remove='muted')).props('outline icon=volume_up')

+ 6 - 0
website/more_documentation/avatar_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.avatar('favorite_border', text_color='grey-11', square=True)
+    ui.avatar('img:https://nicegui.io/logo_square.png', color='blue-2')

+ 6 - 0
website/more_documentation/badge_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.button('Click me!', on_click=lambda: badge.set_text(int(badge.text) + 1)):
+        badge = ui.badge('0', color='red').props('floating')

+ 5 - 0
website/more_documentation/button_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.button('Click me!', on_click=lambda: ui.notify(f'You clicked me!'))

+ 8 - 0
website/more_documentation/card_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.card().tight() as card:
+        ui.image('https://picsum.photos/id/684/640/360')
+        with ui.card_section():
+            ui.label('Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...')

+ 21 - 0
website/more_documentation/chart_documentation.py

@@ -0,0 +1,21 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from numpy.random import random
+
+    chart = ui.chart({
+        'title': False,
+        'chart': {'type': 'bar'},
+        'xAxis': {'categories': ['A', 'B']},
+        'series': [
+            {'name': 'Alpha', 'data': [0.1, 0.2]},
+            {'name': 'Beta', 'data': [0.3, 0.4]},
+        ],
+    }).classes('w-full h-64')
+
+    def update():
+        chart.options['series'][0]['data'][:] = random(2)
+        chart.update()
+
+    ui.button('Update', on_click=update)

+ 6 - 0
website/more_documentation/checkbox_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    checkbox = ui.checkbox('check me')
+    ui.label('Check!').bind_visibility_from(checkbox, 'value')

+ 6 - 0
website/more_documentation/circular_progress_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
+    ui.circular_progress().bind_value_from(slider, 'value')

+ 7 - 0
website/more_documentation/color_input_documentation.py

@@ -0,0 +1,7 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    label = ui.label('Change my color!')
+    ui.color_input(label='Color', value='#000000',
+                   on_change=lambda e: label.style(f'color:{e.value}'))

+ 6 - 0
website/more_documentation/color_picker_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    picker = ui.color_picker(on_pick=lambda e: button.style(f'background-color:{e.color}!important'))
+    button = ui.button(on_click=picker.open).props('icon=colorize')

+ 6 - 0
website/more_documentation/colors_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.button('Default', on_click=lambda: ui.colors())
+    ui.button('Gray', on_click=lambda: ui.colors(primary='#555'))

+ 8 - 0
website/more_documentation/column_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.column():
+        ui.label('label 1')
+        ui.label('label 2')
+        ui.label('label 3')

+ 6 - 0
website/more_documentation/date_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.date(value='2023-01-01', on_change=lambda e: result.set_text(e.value))
+    result = ui.label()

+ 9 - 0
website/more_documentation/dialog_documentation.py

@@ -0,0 +1,9 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.dialog() as dialog, ui.card():
+        ui.label('Hello world!')
+        ui.button('Close', on_click=dialog.close)
+
+    ui.button('Open a dialog', on_click=dialog.open)

+ 6 - 0
website/more_documentation/element_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.element('div').classes('p-2 bg-blue-100'):
+        ui.label('inside a colored div')

+ 6 - 0
website/more_documentation/expansion_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.expansion('Expand!', icon='work').classes('w-full'):
+        ui.label('inside the expansion')

+ 5 - 0
website/more_documentation/html_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.html('This is <strong>HTML</strong>.')

+ 5 - 0
website/more_documentation/icon_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.icon('thumb_up')

+ 5 - 0
website/more_documentation/image_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.image('https://picsum.photos/id/377/640/360')

+ 8 - 0
website/more_documentation/input_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+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()

+ 13 - 0
website/more_documentation/interactive_image_documentation.py

@@ -0,0 +1,13 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from nicegui.events import MouseEventArguments
+
+    def mouse_handler(e: MouseEventArguments):
+        color = 'SkyBlue' if e.type == 'mousedown' else 'SteelBlue'
+        ii.content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="15" fill="none" stroke="{color}" stroke-width="4" />'
+        ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
+
+    src = 'https://picsum.photos/id/565/640/360'
+    ii = ui.interactive_image(src, on_mouse=mouse_handler, events=['mousedown', 'mouseup'], cross=True)

+ 8 - 0
website/more_documentation/joystick_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.joystick(color='blue', size=50,
+                on_move=lambda e: coordinates.set_text(f"{e.x:.3f}, {e.y:.3f}"),
+                on_end=lambda _: coordinates.set_text('0, 0'))
+    coordinates = ui.label('0, 0')

+ 25 - 0
website/more_documentation/keyboard_documentation.py

@@ -0,0 +1,25 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from nicegui.events import KeyEventArguments
+
+    def handle_key(e: KeyEventArguments):
+        if e.key == 'f' and not e.action.repeat:
+            if e.action.keyup:
+                ui.notify('f was just released')
+            elif e.action.keydown:
+                ui.notify('f was just pressed')
+        if e.modifiers.shift and e.action.keydown:
+            if e.key.arrow_left:
+                ui.notify('going left')
+            elif e.key.arrow_right:
+                ui.notify('going right')
+            elif e.key.arrow_up:
+                ui.notify('going up')
+            elif e.key.arrow_down:
+                ui.notify('going down')
+
+    keyboard = ui.keyboard(on_key=handle_key)
+    ui.label('Key events can be caught globally by using the keyboard element.')
+    ui.checkbox('Track key events').bind_value_to(keyboard, 'active')

+ 8 - 0
website/more_documentation/knob_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    knob = ui.knob(0.3, show_value=True)
+
+    with ui.knob(color='orange', track_color='grey-2').bind_value(knob, 'value'):
+        ui.icon('volume_up')

+ 5 - 0
website/more_documentation/label_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.label('some label')

+ 32 - 0
website/more_documentation/line_plot_documentation.py

@@ -0,0 +1,32 @@
+from typing import Dict
+
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from datetime import datetime
+
+    import numpy as np
+
+    line_plot = ui.line_plot(n=2, limit=20, figsize=(3, 2), update_every=5) \
+        .with_legend(['sin', 'cos'], loc='upper center', ncol=2)
+
+    def update_line_plot() -> None:
+        now = datetime.now()
+        x = now.timestamp()
+        y1 = np.sin(x)
+        y2 = np.cos(x)
+        line_plot.push([now], [[y1], [y2]])
+
+    line_updates = ui.timer(0.1, update_line_plot, active=False)
+    line_checkbox = ui.checkbox('active').bind_value(line_updates, 'active')
+
+    # END OF DEMO
+    def handle_change(msg: Dict) -> None:
+        def turn_off() -> None:
+            line_checkbox.set_value(False)
+            ui.notify('Turning off that line plot to save resources on our live demo server. 😎')
+        line_checkbox.value = msg['args']
+        if line_checkbox.value:
+            ui.timer(10.0, turn_off, once=True)
+    line_checkbox.on('update:model-value', handle_change)

+ 6 - 0
website/more_documentation/linear_progress_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
+    ui.linear_progress().bind_value_from(slider, 'value')

+ 5 - 0
website/more_documentation/link_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')

+ 8 - 0
website/more_documentation/log_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from datetime import datetime
+
+    log = ui.log(max_lines=10).classes('w-full h-20')
+    ui.button('Log time', on_click=lambda: log.push(datetime.now().strftime('%X.%f')[:-5]))

+ 41 - 0
website/more_documentation/markdown_documentation.py

@@ -0,0 +1,41 @@
+from nicegui import ui
+
+from ..documentation_tools import text_demo
+
+
+def main_demo() -> None:
+    ui.markdown('''This is **Markdown**.''')
+
+
+def more() -> None:
+    @text_demo('Markdown with indentation', '''
+        Common indentation is automatically stripped from the beginning of each line.
+        So you can indent markdown elements, and they will still be rendered correctly.
+    ''')
+    def markdown_with_indentation():
+        ui.markdown('''
+            ### Example
+
+            This line is not indented.
+
+                This block is indented.
+                Thus it is rendered as source code.
+            
+            This is normal text again.
+        ''')
+
+    @text_demo('Markdown with code blocks', '''
+        You can use code blocks to show code examples.
+        If you specify the language after the opening triple backticks, the code will be syntax highlighted.
+        See [the Pygments website](https://pygments.org/languages/) for a list of supported languages.
+    ''')
+    def markdown_with_code_blocks():
+        ui.markdown('''
+            ```python
+            from nicegui import ui
+
+            ui.label('Hello World!')
+
+            ui.run(dark=True)
+            ```
+        ''')

+ 14 - 0
website/more_documentation/menu_documentation.py

@@ -0,0 +1,14 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.row().classes('w-full items-center'):
+        result = ui.label().classes('mr-auto')
+        with ui.button(on_click=lambda: menu.open()).props('icon=menu'):
+            with ui.menu() as menu:
+                ui.menu_item('Menu item 1', lambda: result.set_text('Selected item 1'))
+                ui.menu_item('Menu item 2', lambda: result.set_text('Selected item 2'))
+                ui.menu_item('Menu item 3 (keep open)',
+                             lambda: result.set_text('Selected item 3'), auto_close=False)
+                ui.separator()
+                ui.menu_item('Close', on_click=menu.close)

+ 9 - 0
website/more_documentation/mermaid_documentation.py

@@ -0,0 +1,9 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.mermaid('''
+    graph LR;
+        A --> B;
+        A --> C;
+    ''')

+ 5 - 0
website/more_documentation/notify_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.button('Say hi!', on_click=lambda: ui.notify('Hi!', close_button='OK'))

+ 7 - 0
website/more_documentation/number_documentation.py

@@ -0,0 +1,7 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.number(label='Number', value=3.1415927, format='%.2f',
+              on_change=lambda e: result.set_text(f'you entered: {e.value}'))
+    result = ui.label()

+ 10 - 0
website/more_documentation/open_documentation.py

@@ -0,0 +1,10 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    @ui.page('/yet_another_page')
+    def yet_another_page():
+        ui.label('Welcome to yet another page')
+        ui.button('RETURN', on_click=lambda: ui.open('documentation#open'))
+
+    ui.button('REDIRECT', on_click=lambda: ui.open(yet_another_page))

+ 16 - 0
website/more_documentation/page_documentation.py

@@ -0,0 +1,16 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    @ui.page('/other_page')
+    def other_page():
+        ui.label('Welcome to the other side')
+        ui.link('Back to main page', '/documentation#page')
+
+    @ui.page('/dark_page', dark=True)
+    def dark_page():
+        ui.label('Welcome to the dark side')
+        ui.link('Back to main page', '/documentation#page')
+
+    ui.link('Visit other page', other_page)
+    ui.link('Visit dark page', dark_page)

+ 9 - 0
website/more_documentation/plotly_documentation.py

@@ -0,0 +1,9 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    import plotly.graph_objects as go
+
+    fig = go.Figure(go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 3, 2.5]))
+    fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
+    ui.plotly(fig).classes('w-full h-40')

+ 11 - 0
website/more_documentation/pyplot_documentation.py

@@ -0,0 +1,11 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    import numpy as np
+    from matplotlib import pyplot as plt
+
+    with ui.pyplot(figsize=(3, 2)):
+        x = np.linspace(0.0, 5.0)
+        y = np.cos(2 * np.pi * x) * np.exp(-x)
+        plt.plot(x, y, '-')

+ 6 - 0
website/more_documentation/radio_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    radio1 = ui.radio([1, 2, 3], value=1).props('inline')
+    radio2 = ui.radio({1: 'A', 2: 'B', 3: 'C'}).props('inline').bind_value(radio1, 'value')

+ 8 - 0
website/more_documentation/row_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.row():
+        ui.label('label 1')
+        ui.label('label 2')
+        ui.label('label 3')

+ 7 - 0
website/more_documentation/run_documentation.py

@@ -0,0 +1,7 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.label('page with custom title')
+
+    # ui.run(title='My App')

+ 18 - 0
website/more_documentation/run_javascript_documentation.py

@@ -0,0 +1,18 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    async def alert():
+        await ui.run_javascript('alert("Hello!")', respond=False)
+
+    async def get_date():
+        time = await ui.run_javascript('Date()')
+        ui.notify(f'Browser time: {time}')
+
+    async def access_elements():
+        await ui.run_javascript(f'getElement({label.id}).innerText += " Hello!"')
+
+    ui.button('fire and forget', on_click=alert)
+    ui.button('receive result', on_click=get_date)
+    ui.button('access elements', on_click=access_elements)
+    label = ui.label()

+ 26 - 0
website/more_documentation/scene_documentation.py

@@ -0,0 +1,26 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.scene(width=285, height=285) as scene:
+        scene.sphere().material('#4488ff')
+        scene.cylinder(1, 0.5, 2, 20).material('#ff8800', opacity=0.5).move(-2, 1)
+        scene.extrusion([[0, 0], [0, 1], [1, 0.5]], 0.1).material('#ff8888').move(-2, -2)
+
+        with scene.group().move(z=2):
+            scene.box().move(x=2)
+            scene.box().move(y=2).rotate(0.25, 0.5, 0.75)
+            scene.box(wireframe=True).material('#888888').move(x=2, y=2)
+
+        scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000')
+        scene.curve([-4, 0, 0], [-4, -1, 0], [-3, -1, 0], [-3, -2, 0]).material('#008800')
+
+        logo = 'https://avatars.githubusercontent.com/u/2843826'
+        scene.texture(logo, [[[0.5, 2, 0], [2.5, 2, 0]],
+                             [[0.5, 0, 0], [2.5, 0, 0]]]).move(1, -2)
+
+        teapot = 'https://upload.wikimedia.org/wikipedia/commons/9/93/Utah_teapot_(solid).stl'
+        scene.stl(teapot).scale(0.2).move(-3, 4)
+
+        scene.text('2D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(z=2)
+        scene.text3d('3D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(y=-2).scale(.05)

+ 27 - 0
website/more_documentation/select_documentation.py

@@ -0,0 +1,27 @@
+from nicegui import ui
+
+from ..documentation_tools import text_demo
+
+
+def main_demo() -> None:
+    select1 = ui.select([1, 2, 3], value=1)
+    select2 = ui.select({1: 'One', 2: 'Two', 3: 'Three'}).bind_value(select1, 'value')
+
+
+def more() -> None:
+    @text_demo('Search-as-you-type', '''
+        You can activate `with_input` to get a text input with autocompletion.
+        The options will be filtered as you type.
+    ''')
+    def search_as_you_type():
+        continents = [
+            'Asia',
+            'Africa',
+            'Antarctica',
+            'Europe',
+            'Oceania',
+            'North America',
+            'South America',
+        ]
+        ui.select(options=continents, with_input=True,
+                  on_change=lambda e: ui.notify(e.value)).classes('w-40')

+ 12 - 0
website/more_documentation/shutdown_documentation.py

@@ -0,0 +1,12 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    from nicegui import app
+
+    # ui.button('shutdown', on_click=app.shutdown)
+    #
+    # ui.run(reload=False)
+    # END OF DEMO
+    ui.button('shutdown', on_click=lambda: ui.notify(
+        'Nah. We do not actually shutdown the documentation server. Try it in your own app!'))

+ 6 - 0
website/more_documentation/slider_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    slider = ui.slider(min=0, max=100, value=50)
+    ui.label().bind_text_from(slider, 'value')

+ 8 - 0
website/more_documentation/spinner_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    with ui.row():
+        ui.spinner(size='lg')
+        ui.spinner('audio', size='lg', color='green')
+        ui.spinner('dots', size='lg', color='red')

+ 6 - 0
website/more_documentation/switch_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    switch = ui.switch('switch me')
+    ui.label('Switch!').bind_visibility_from(switch, 'value')

+ 60 - 0
website/more_documentation/table_documentation.py

@@ -0,0 +1,60 @@
+from nicegui import ui
+
+from ..documentation_tools import text_demo
+
+
+def main_demo() -> None:
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
+        {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carol'},
+    ]
+    ui.table(columns=columns, rows=rows, row_key='name')
+
+
+def more() -> None:
+    @text_demo('Table with expandable rows', '''
+        Scoped slots can be used to insert buttons that toggle the expand state of a table row.
+        See the [Quasar documentation](https://quasar.dev/vue-components/table#expanding-rows) for more information.
+    ''')
+    def table_with_expandable_rows():
+        columns = [
+            {'name': 'name', 'label': 'Name', 'field': 'name'},
+            {'name': 'age', 'label': 'Age', 'field': 'age'},
+        ]
+        rows = [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol'},
+        ]
+
+        with ui.table(columns=columns, rows=rows, row_key='name').classes('w-72') as table:
+            table.add_slot('header', r'''
+                <q-tr :props="props">
+                    <q-th auto-width />
+                    <q-th v-for="col in props.cols" :key="col.name" :props="props">
+                        {{ col.label }}
+                    </q-th>
+                </q-tr>
+            ''')
+            table.add_slot('body', r'''
+                <q-tr :props="props">
+                    <q-td auto-width>
+                        <q-btn size="sm" color="accent" round dense
+                            @click="props.expand = !props.expand"
+                            :icon="props.expand ? 'remove' : 'add'" />
+                    </q-td>
+                    <q-td v-for="col in props.cols" :key="col.name" :props="props">
+                        {{ col.value }}
+                    </q-td>
+                </q-tr>
+                <q-tr v-show="props.expand" :props="props">
+                    <q-td colspan="100%">
+                        <div class="text-left">This is {{ props.row.name }}.</div>
+                    </q-td>
+                </q-tr>
+            ''')

+ 7 - 0
website/more_documentation/textarea_documentation.py

@@ -0,0 +1,7 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.textarea(label='Text', placeholder='start typing',
+                on_change=lambda e: result.set_text('you typed: ' + e.value))
+    result = ui.label()

+ 6 - 0
website/more_documentation/time_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.time(value='12:00', on_change=lambda e: result.set_text(e.value))
+    result = ui.label()

+ 19 - 0
website/more_documentation/timer_documentation.py

@@ -0,0 +1,19 @@
+from nicegui import ui
+
+
+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')
+
+    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)

+ 6 - 0
website/more_documentation/toggle_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    toggle1 = ui.toggle([1, 2, 3], value=1)
+    toggle2 = ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(toggle1, 'value')

+ 8 - 0
website/more_documentation/tree_documentation.py

@@ -0,0 +1,8 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.tree([
+        {'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
+        {'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
+    ], label_key='id', on_select=lambda e: ui.notify(e.value))

+ 5 - 0
website/more_documentation/upload_documentation.py

@@ -0,0 +1,5 @@
+from nicegui import ui
+
+
+def main_demo() -> None:
+    ui.upload(on_upload=lambda e: ui.notify(f'Uploaded {e.name}')).classes('max-w-full')

+ 6 - 0
website/more_documentation/video_documentation.py

@@ -0,0 +1,6 @@
+from nicegui import ui
+
+
+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.on('ended', lambda _: ui.notify('Video playback completed'))

+ 0 - 1074
website/reference.py

@@ -1,1074 +0,0 @@
-import uuid
-from typing import Dict
-
-from nicegui import app, ui
-from nicegui.elements.markdown import prepare_content
-
-from .example import add_html_with_anchor_link, bash_window, example, python_window
-
-CONSTANT_UUID = str(uuid.uuid4())
-
-
-def create_intro() -> None:
-    @example('''#### Styling
-
-While having reasonable defaults, you can still modify the look of your app with CSS as well as Tailwind and Quasar classes.
-''', None)
-    def formatting_example():
-        ui.icon('thumb_up')
-        ui.markdown('This is **Markdown**.')
-        ui.html('This is <strong>HTML</strong>.')
-        with ui.row():
-            ui.label('CSS').style('color: #888; font-weight: bold')
-            ui.label('Tailwind').classes('font-serif')
-            ui.label('Quasar').classes('q-ml-xl')
-        ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
-
-    @example('''#### Common UI Elements
-
-NiceGUI comes with a collection of commonly used UI elements.
-''', None)
-    def common_elements_example():
-        from nicegui.events import ValueChangeEventArguments
-
-        def show(event: ValueChangeEventArguments):
-            name = type(event.sender).__name__
-            ui.notify(f'{name}: {event.value}')
-
-        ui.button('Button', on_click=lambda: ui.notify('Click'))
-        with ui.row():
-            ui.checkbox('Checkbox', on_change=show)
-            ui.switch('Switch', on_change=show)
-        ui.radio(['A', 'B', 'C'], value='A', on_change=show).props('inline')
-        with ui.row():
-            ui.input('Text input', on_change=show)
-            ui.select(['One', 'Two'], value='One', on_change=show)
-        ui.link('And many more...', '/reference').classes('mt-8')
-
-    @example('''#### Value Binding
-
-Binding values between UI elements and data models is built into NiceGUI.
-''', None)
-    def binding_example():
-        class Demo:
-            def __init__(self):
-                self.number = 1
-
-        demo = Demo()
-        v = ui.checkbox('visible', value=True)
-        with ui.column().bind_visibility_from(v, 'value'):
-            ui.slider(min=1, max=3).bind_value(demo, 'number')
-            ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
-            ui.number().bind_value(demo, 'number')
-
-
-def create_full(menu: ui.element) -> None:
-    def h3(text: str) -> None:
-        ui.html(f'<em>{text}</em>').classes('mt-8 text-3xl font-weight-500')
-        with menu:
-            ui.label(text).classes('font-bold mt-4')
-
-    def add_markdown_with_headline(content: str):
-        add_html_with_anchor_link(prepare_content(content, 'fenced-code-blocks'), menu)
-
-    h3('Basic Elements')
-
-    @example(ui.label, menu)
-    def label_example():
-        ui.label('some label')
-
-    @example(ui.icon, menu)
-    def icon_example():
-        ui.icon('thumb_up')
-
-    @example(ui.avatar, menu)
-    def avatar_example():
-        ui.avatar('favorite_border', text_color='grey-11', square=True)
-        ui.avatar('img:https://nicegui.io/logo_square.png', color='blue-2')
-
-    @example(ui.link, menu)
-    def link_example():
-        ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
-
-    @example(ui.button, menu)
-    def button_example():
-        ui.button('Click me!', on_click=lambda: ui.notify(f'You clicked me!'))
-
-    @example(ui.badge, menu)
-    def badge_example():
-        with ui.button('Click me!', on_click=lambda: badge.set_text(int(badge.text) + 1)):
-            badge = ui.badge('0', color='red').props('floating')
-
-    @example(ui.toggle, menu)
-    def toggle_example():
-        toggle1 = ui.toggle([1, 2, 3], value=1)
-        toggle2 = ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(toggle1, 'value')
-
-    @example(ui.radio, menu)
-    def radio_example():
-        radio1 = ui.radio([1, 2, 3], value=1).props('inline')
-        radio2 = ui.radio({1: 'A', 2: 'B', 3: 'C'}).props('inline').bind_value(radio1, 'value')
-
-    @example(ui.select, menu)
-    def select_example():
-        select1 = ui.select([1, 2, 3], value=1)
-        select2 = ui.select({1: 'One', 2: 'Two', 3: 'Three'}).bind_value(select1, 'value')
-
-    @example(ui.checkbox, menu)
-    def checkbox_example():
-        checkbox = ui.checkbox('check me')
-        ui.label('Check!').bind_visibility_from(checkbox, 'value')
-
-    @example(ui.switch, menu)
-    def switch_example():
-        switch = ui.switch('switch me')
-        ui.label('Switch!').bind_visibility_from(switch, 'value')
-
-    @example(ui.slider, menu)
-    def slider_example():
-        slider = ui.slider(min=0, max=100, value=50)
-        ui.label().bind_text_from(slider, 'value')
-
-    @example(ui.joystick, menu)
-    def joystick_example():
-        ui.joystick(color='blue', size=50,
-                    on_move=lambda e: coordinates.set_text(f"{e.x:.3f}, {e.y:.3f}"),
-                    on_end=lambda _: coordinates.set_text('0, 0'))
-        coordinates = ui.label('0, 0')
-
-    @example(ui.input, menu)
-    def input_example():
-        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()
-
-    @example(ui.textarea, menu)
-    def textarea_example():
-        ui.textarea(label='Text', placeholder='start typing',
-                    on_change=lambda e: result.set_text('you typed: ' + e.value))
-        result = ui.label()
-
-    @example(ui.number, menu)
-    def number_example():
-        ui.number(label='Number', value=3.1415927, format='%.2f',
-                  on_change=lambda e: result.set_text(f'you entered: {e.value}'))
-        result = ui.label()
-
-    @example(ui.knob, menu)
-    def knob_example():
-        knob = ui.knob(0.3, show_value=True)
-
-        with ui.knob(color='orange', track_color='grey-2').bind_value(knob, 'value'):
-            ui.icon('volume_up')
-
-    @example(ui.color_input, menu)
-    def color_input_example():
-        label = ui.label('Change my color!')
-        ui.color_input(label='Color', value='#000000',
-                       on_change=lambda e: label.style(f'color:{e.value}'))
-
-    @example(ui.color_picker, menu)
-    def color_picker_example():
-        picker = ui.color_picker(on_pick=lambda e: button.style(f'background-color:{e.color}!important'))
-        button = ui.button(on_click=picker.open).props('icon=colorize')
-
-    @example(ui.date, menu)
-    def date_example():
-        ui.date(value='2023-01-01', on_change=lambda e: result.set_text(e.value))
-        result = ui.label()
-
-    @example(ui.time, menu)
-    def time_example():
-        ui.time(value='12:00', on_change=lambda e: result.set_text(e.value))
-        result = ui.label()
-
-    @example(ui.upload, menu)
-    def upload_example():
-        ui.upload(on_upload=lambda e: ui.notify(f'Uploaded {e.name}')).classes('max-w-full')
-
-    h3('Markdown and HTML')
-
-    @example(ui.markdown, menu)
-    def markdown_example():
-        ui.markdown('''This is **Markdown**.''')
-
-    @example(ui.mermaid, menu)
-    def mermaid_example():
-        ui.mermaid('''
-        graph LR;
-            A --> B;
-            A --> C;
-        ''')
-
-    @example(ui.html, menu)
-    def html_example():
-        ui.html('This is <strong>HTML</strong>.')
-
-    @example('''#### SVG
-
-You can add Scalable Vector Graphics using the `ui.html` element.
-''', menu)
-    def svg_example():
-        content = '''
-            <svg viewBox="0 0 200 200" width="100" height="100" 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.html(content)
-
-    h3('Images, Audio and Video')
-
-    @example(ui.image, menu)
-    def image_example():
-        ui.image('https://picsum.photos/id/377/640/360')
-
-    @example('''#### Captions and Overlays
-
-By nesting elements inside a `ui.image` you can create augmentations.
-
-Use [Quasar classes](https://quasar.dev/vue-components/img) for positioning and styling captions.
-To overlay an SVG, make the `viewBox` exactly the size of the image and provide `100%` width/height to match the actual rendered size.
-''', menu)
-    def captions_and_overlays_example():
-        with ui.image('https://picsum.photos/id/29/640/360'):
-            ui.label('Nice!').classes('absolute-bottom text-subtitle2 text-center')
-
-        with ui.image('https://cdn.stocksnap.io/img-thumbs/960w/airplane-sky_DYPWDEEILG.jpg'):
-            ui.html('''
-                <svg viewBox="0 0 960 638" width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
-                <circle cx="445" cy="300" r="100" fill="none" stroke="red" stroke-width="20" />
-                </svg>
-            ''').classes('bg-transparent')
-
-    @example(ui.interactive_image, menu)
-    def interactive_image_example():
-        from nicegui.events import MouseEventArguments
-
-        def mouse_handler(e: MouseEventArguments):
-            color = 'SkyBlue' if e.type == 'mousedown' else 'SteelBlue'
-            ii.content += f'<circle cx="{e.image_x}" cy="{e.image_y}" r="15" fill="none" stroke="{color}" stroke-width="4" />'
-            ui.notify(f'{e.type} at ({e.image_x:.1f}, {e.image_y:.1f})')
-
-        src = 'https://picsum.photos/id/565/640/360'
-        ii = ui.interactive_image(src, on_mouse=mouse_handler, events=['mousedown', 'mouseup'], cross=True)
-
-    @example(ui.audio, menu)
-    def image_example():
-        a = ui.audio('https://cdn.pixabay.com/download/audio/2022/02/22/audio_d1718ab41b.mp3')
-        a.on('ended', lambda _: ui.notify('Audio playback completed'))
-
-        ui.button(on_click=lambda: a.props('muted')).props('outline icon=volume_off')
-        ui.button(on_click=lambda: a.props(remove='muted')).props('outline icon=volume_up')
-
-    @example(ui.video, menu)
-    def image_example():
-        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'))
-
-    h3('Data Elements')
-
-    @example(ui.aggrid, menu)
-    def aggrid_example():
-        grid = ui.aggrid({
-            'columnDefs': [
-                {'headerName': 'Name', 'field': 'name'},
-                {'headerName': 'Age', 'field': 'age'},
-            ],
-            'rowData': [
-                {'name': 'Alice', 'age': 18},
-                {'name': 'Bob', 'age': 21},
-                {'name': 'Carol', 'age': 42},
-            ],
-            'rowSelection': 'multiple',
-        }).classes('max-h-40')
-
-        def update():
-            grid.options['rowData'][0]['age'] += 1
-            grid.update()
-
-        ui.button('Update', on_click=update)
-        ui.button('Select all', on_click=lambda: grid.call_api_method('selectAll'))
-
-    @example(ui.table, menu)
-    def table_example():
-        columns = [
-            {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
-            {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
-        ]
-        rows = [
-            {'name': 'Alice', 'age': 18},
-            {'name': 'Bob', 'age': 21},
-            {'name': 'Carol'},
-        ]
-        ui.table(columns=columns, rows=rows, row_key='name')
-
-    @example(ui.chart, menu)
-    def chart_example():
-        from numpy.random import random
-
-        chart = ui.chart({
-            'title': False,
-            'chart': {'type': 'bar'},
-            'xAxis': {'categories': ['A', 'B']},
-            'series': [
-                {'name': 'Alpha', 'data': [0.1, 0.2]},
-                {'name': 'Beta', 'data': [0.3, 0.4]},
-            ],
-        }).classes('w-full h-64')
-
-        def update():
-            chart.options['series'][0]['data'][:] = random(2)
-            chart.update()
-
-        ui.button('Update', on_click=update)
-
-    @example(ui.pyplot, menu)
-    def plot_example():
-        import numpy as np
-        from matplotlib import pyplot as plt
-
-        with ui.pyplot(figsize=(3, 2)):
-            x = np.linspace(0.0, 5.0)
-            y = np.cos(2 * np.pi * x) * np.exp(-x)
-            plt.plot(x, y, '-')
-
-    @example(ui.line_plot, menu)
-    def line_plot_example():
-        from datetime import datetime
-
-        import numpy as np
-
-        line_plot = ui.line_plot(n=2, limit=20, figsize=(3, 2), update_every=5) \
-            .with_legend(['sin', 'cos'], loc='upper center', ncol=2)
-
-        def update_line_plot() -> None:
-            now = datetime.now()
-            x = now.timestamp()
-            y1 = np.sin(x)
-            y2 = np.cos(x)
-            line_plot.push([now], [[y1], [y2]])
-
-        line_updates = ui.timer(0.1, update_line_plot, active=False)
-        line_checkbox = ui.checkbox('active').bind_value(line_updates, 'active')
-
-        # END OF EXAMPLE
-        def handle_change(msg: Dict) -> None:
-            def turn_off() -> None:
-                line_checkbox.set_value(False)
-                ui.notify('Turning off that line plot to save resources on our live demo server. 😎')
-            line_checkbox.value = msg['args']
-            if line_checkbox.value:
-                ui.timer(10.0, turn_off, once=True)
-        line_checkbox.on('update:model-value', handle_change)
-
-    @example(ui.plotly, menu)
-    def plotly_example():
-        import plotly.graph_objects as go
-
-        fig = go.Figure(go.Scatter(x=[1, 2, 3, 4], y=[1, 2, 3, 2.5]))
-        fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
-        ui.plotly(fig).classes('w-full h-40')
-
-    @example(ui.linear_progress, menu)
-    def linear_progress_example():
-        slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
-        ui.linear_progress().bind_value_from(slider, 'value')
-
-    @example(ui.circular_progress, menu)
-    def circular_progress_example():
-        slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
-        ui.circular_progress().bind_value_from(slider, 'value')
-
-    @example(ui.spinner, menu)
-    def spinner_example():
-        with ui.row():
-            ui.spinner(size='lg')
-            ui.spinner('audio', size='lg', color='green')
-            ui.spinner('dots', size='lg', color='red')
-
-    @example(ui.scene, menu)
-    def scene_example():
-        with ui.scene(width=285, height=285) as scene:
-            scene.sphere().material('#4488ff')
-            scene.cylinder(1, 0.5, 2, 20).material('#ff8800', opacity=0.5).move(-2, 1)
-            scene.extrusion([[0, 0], [0, 1], [1, 0.5]], 0.1).material('#ff8888').move(-2, -2)
-
-            with scene.group().move(z=2):
-                scene.box().move(x=2)
-                scene.box().move(y=2).rotate(0.25, 0.5, 0.75)
-                scene.box(wireframe=True).material('#888888').move(x=2, y=2)
-
-            scene.line([-4, 0, 0], [-4, 2, 0]).material('#ff0000')
-            scene.curve([-4, 0, 0], [-4, -1, 0], [-3, -1, 0], [-3, -2, 0]).material('#008800')
-
-            logo = 'https://avatars.githubusercontent.com/u/2843826'
-            scene.texture(logo, [[[0.5, 2, 0], [2.5, 2, 0]],
-                                 [[0.5, 0, 0], [2.5, 0, 0]]]).move(1, -2)
-
-            teapot = 'https://upload.wikimedia.org/wikipedia/commons/9/93/Utah_teapot_(solid).stl'
-            scene.stl(teapot).scale(0.2).move(-3, 4)
-
-            scene.text('2D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(z=2)
-            scene.text3d('3D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(y=-2).scale(.05)
-
-    @example(ui.tree, menu)
-    def tree_example():
-        ui.tree([
-            {'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
-            {'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
-        ], label_key='id', on_select=lambda e: ui.notify(e.value))
-
-    @example(ui.log, menu)
-    def log_example():
-        from datetime import datetime
-
-        log = ui.log(max_lines=10).classes('w-full h-20')
-        ui.button('Log time', on_click=lambda: log.push(datetime.now().strftime('%X.%f')[:-5]))
-
-    h3('Layout')
-
-    @example(ui.card, menu)
-    def card_example():
-        with ui.card().tight() as card:
-            ui.image('https://picsum.photos/id/684/640/360')
-            with ui.card_section():
-                ui.label('Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...')
-
-    @example(ui.column, menu)
-    def column_example():
-        with ui.column():
-            ui.label('label 1')
-            ui.label('label 2')
-            ui.label('label 3')
-
-    @example(ui.row, menu)
-    def row_example():
-        with ui.row():
-            ui.label('label 1')
-            ui.label('label 2')
-            ui.label('label 3')
-
-    @example('''#### Clear Containers
-
-To remove all elements from a row, column or card container, use the `clear()` method.
-
-Alternatively, you can remove individual elements with `remove(element)`, where `element` is an Element or an index.
-''', menu)
-    def clear_containers_example():
-        container = ui.row()
-
-        def add_face():
-            with container:
-                ui.icon('face')
-        add_face()
-
-        ui.button('Add', on_click=add_face)
-        ui.button('Remove', on_click=lambda: container.remove(0))
-        ui.button('Clear', on_click=container.clear)
-
-    @example(ui.expansion, menu)
-    def expansion_example():
-        with ui.expansion('Expand!', icon='work').classes('w-full'):
-            ui.label('inside the expansion')
-
-    @example('''#### Tabs
-
-The elements `ui.tabs`, `ui.tab`, `ui.tab_panels`, and `ui.tab_panel` resemble
-[Quasar's tabs](https://quasar.dev/vue-components/tabs)
-and [tab panels](https://quasar.dev/vue-components/tab-panels) API.
-
-`ui.tabs` creates a container for the tabs. This could be placed in a `ui.header` for example.
-`ui.tab_panels` creates a container for the tab panels with the actual content.
-''', menu)
-    def tabs_example():
-        with ui.tabs() as tabs:
-            ui.tab('Home', icon='home')
-            ui.tab('About', icon='info')
-
-        with ui.tab_panels(tabs, value='Home'):
-            with ui.tab_panel('Home'):
-                ui.label('This is the first tab')
-            with ui.tab_panel('About'):
-                ui.label('This is the second tab')
-
-    @example(ui.menu, menu)
-    def menu_example():
-        with ui.row().classes('w-full items-center'):
-            result = ui.label().classes('mr-auto')
-            with ui.button(on_click=lambda: menu.open()).props('icon=menu'):
-                with ui.menu() as menu:
-                    ui.menu_item('Menu item 1', lambda: result.set_text('Selected item 1'))
-                    ui.menu_item('Menu item 2', lambda: result.set_text('Selected item 2'))
-                    ui.menu_item('Menu item 3 (keep open)',
-                                 lambda: result.set_text('Selected item 3'), auto_close=False)
-                    ui.separator()
-                    ui.menu_item('Close', on_click=menu.close)
-
-    @example('''#### Tooltips
-
-Simply call the `tooltip(text:str)` method on UI elements to provide a tooltip.
-
-For more artistic control you can nest tooltip elements and apply props, classes and styles.
-''', menu)
-    def tooltips_example():
-        ui.label('Tooltips...').tooltip('...are shown on mouse over')
-        with ui.button().props('icon=thumb_up'):
-            ui.tooltip('I like this').classes('bg-green')
-
-    @example(ui.notify, menu)
-    def notify_example():
-        ui.button('Say hi!', on_click=lambda: ui.notify('Hi!', close_button='OK'))
-
-    @example(ui.dialog, menu)
-    def dialog_example():
-        with ui.dialog() as dialog, ui.card():
-            ui.label('Hello world!')
-            ui.button('Close', on_click=dialog.close)
-
-        ui.button('Open a dialog', on_click=dialog.open)
-
-    @example('''#### Awaitable dialog
-
-Dialogs can be awaited.
-Use the `submit` method to close the dialog and return a result.
-Canceling the dialog by clicking in the background or pressing the escape key yields `None`.
-''', menu)
-    def async_dialog_example():
-        with ui.dialog() as dialog, ui.card():
-            ui.label('Are you sure?')
-            with ui.row():
-                ui.button('Yes', on_click=lambda: dialog.submit('Yes'))
-                ui.button('No', on_click=lambda: dialog.submit('No'))
-
-        async def show():
-            result = await dialog
-            ui.notify(f'You chose {result}')
-
-        ui.button('Await a dialog', on_click=show)
-
-    h3('Appearance')
-
-    @example('''#### Styling
-
-NiceGUI uses the [Quasar Framework](https://quasar.dev/) version 1.0 and hence has its full design power.
-Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
-Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
-You can also apply [Tailwind](https://tailwindcss.com/) utility classes with the `classes` method.
-
-If you really need to apply CSS, you can use the `styles` method. Here the delimiter is `;` instead of a blank space.
-
-All three functions also provide `remove` and `replace` parameters in case the predefined look is not wanted in a particular styling.
-''', menu)
-    def design_example():
-        ui.radio(['x', 'y', 'z'], value='x').props('inline color=green')
-        ui.button().props('icon=touch_app outline round').classes('shadow-lg')
-        ui.label('Stylish!').style('color: #6E93D6; font-size: 200%; font-weight: 300')
-
-    @example(ui.colors, menu)
-    def colors_example():
-        ui.button('Default', on_click=lambda: ui.colors())
-        ui.button('Gray', on_click=lambda: ui.colors(primary='#555'))
-
-    h3('Action')
-
-    @example(ui.timer, menu)
-    def timer_example():
-        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')
-
-        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)
-
-    @example(ui.keyboard, menu)
-    def keyboard_example():
-        from nicegui.events import KeyEventArguments
-
-        def handle_key(e: KeyEventArguments):
-            if e.key == 'f' and not e.action.repeat:
-                if e.action.keyup:
-                    ui.notify('f was just released')
-                elif e.action.keydown:
-                    ui.notify('f was just pressed')
-            if e.modifiers.shift and e.action.keydown:
-                if e.key.arrow_left:
-                    ui.notify('going left')
-                elif e.key.arrow_right:
-                    ui.notify('going right')
-                elif e.key.arrow_up:
-                    ui.notify('going up')
-                elif e.key.arrow_down:
-                    ui.notify('going down')
-
-        keyboard = ui.keyboard(on_key=handle_key)
-        ui.label('Key events can be caught globally by using the keyboard element.')
-        ui.checkbox('Track key events').bind_value_to(keyboard, 'active')
-
-    @example('''#### Bindings
-
-NiceGUI is able to directly bind UI elements to models.
-Binding is possible for UI element properties like text, value or visibility and for model properties that are (nested) class attributes.
-
-Each element provides methods like `bind_value` and `bind_visibility` to create a two-way binding with the corresponding property.
-To define a one-way binding use the `_from` and `_to` variants of these methods.
-Just pass a property of the model as parameter to these methods to create the binding.
-''', menu)
-    def bindings_example():
-        class Demo:
-            def __init__(self):
-                self.number = 1
-
-        demo = Demo()
-        v = ui.checkbox('visible', value=True)
-        with ui.column().bind_visibility_from(v, 'value'):
-            ui.slider(min=1, max=3).bind_value(demo, 'number')
-            ui.toggle({1: 'A', 2: 'B', 3: 'C'}).bind_value(demo, 'number')
-            ui.number().bind_value(demo, 'number')
-
-    @example('''#### UI Updates
-
-NiceGUI tries to automatically synchronize the state of UI elements with the client, e.g. when a label text, an input value or style/classes/props of an element have changed.
-In other cases, you can explicitly call `element.update()` or `ui.update(*elements)` to update.
-The example code shows both methods for a `ui.chart`, where it is difficult to automatically detect changes in the `options` dictionary.
-''', menu)
-    def ui_updates_example():
-        from random import randint
-
-        chart = ui.chart({'title': False, 'series': [{'data': [1, 2]}]}).classes('w-full h-64')
-
-        def add():
-            chart.options['series'][0]['data'].append(randint(0, 100))
-            chart.update()
-
-        def clear():
-            chart.options['series'][0]['data'].clear()
-            ui.update(chart)
-
-        with ui.row():
-            ui.button('Add', on_click=add)
-            ui.button('Clear', on_click=clear)
-
-    @example('''#### Async event handlers
-
-Most elements also support asynchronous event handlers.
-
-Note: You can also pass a `functools.partial` into the `on_click` property to wrap async functions with parameters.
-''', menu)
-    def async_handlers_example():
-        import asyncio
-
-        async def async_task():
-            ui.notify('Asynchronous task started')
-            await asyncio.sleep(5)
-            ui.notify('Asynchronous task finished')
-
-        ui.button('start async task', on_click=async_task)
-
-    h3('Pages')
-
-    @example(ui.page, menu)
-    def page_example():
-        @ui.page('/other_page')
-        def other_page():
-            ui.label('Welcome to the other side')
-            ui.link('Back to main page', '/reference#page')
-
-        @ui.page('/dark_page', dark=True)
-        def dark_page():
-            ui.label('Welcome to the dark side')
-            ui.link('Back to main page', '/reference#page')
-
-        ui.link('Visit other page', other_page)
-        ui.link('Visit dark page', dark_page)
-
-    @example('''#### Auto-index page
-
-Pages created with the `@ui.page` decorator are "private".
-Their content is re-created for each client.
-Thus, in the example to the right, the displayed ID on the private page changes when the browser reloads the page.
-
-UI elements that are not wrapped in a decorated page function are placed on an automatically generated index page at route "/".
-This auto-index page is created once on startup and *shared* across all clients that might connect.
-Thus, each connected client will see the *same* elements.
-In the example to the right, the displayed ID on the auto-index page remains constant when the browser reloads the page.
-''', menu)
-    def auto_index_page():
-        from uuid import uuid4
-
-        @ui.page('/private_page')
-        async def private_page():
-            ui.label(f'private page with ID {uuid4()}')
-
-        # ui.label(f'shared auto-index page with ID {uuid4()}')
-        # ui.link('private page', private_page)
-        # END OF EXAMPLE
-        ui.label(f'shared auto-index page with ID {CONSTANT_UUID}')
-        ui.link('private page', private_page)
-
-    @example('''#### Pages with Path Parameters
-
-Page routes can contain parameters like [FastAPI](https://fastapi.tiangolo.com/tutorial/path-params/>).
-If type-annotated, they are automatically converted to bool, int, float and complex values.
-If the page function expects a `request` argument, the request object is automatically provided.
-The `client` argument provides access to the websocket connection, layout, etc.
-''', menu)
-    def page_with_path_parameters_example():
-        @ui.page('/repeat/{word}/{count}')
-        def page(word: str, count: int):
-            ui.label(word * count)
-
-        ui.link('Say hi to Santa!', 'repeat/Ho! /3')
-
-    @example('''#### Wait for Client Connection
-
-To wait for a client connection, you can add a `client` argument to the decorated page function
-and await `client.connected()`.
-All code below that statement is executed after the websocket connection between server and client has been established.
-
-For example, this allows you to run JavaScript commands; which is only possible with a client connection (see [#112](https://github.com/zauberzeug/nicegui/issues/112)).
-Also it is possible to do async stuff while the user already sees some content.
-''', menu)
-    def wait_for_connected_example():
-        import asyncio
-
-        from nicegui import Client
-
-        @ui.page('/wait_for_connection')
-        async def wait_for_connection(client: Client):
-            ui.label('This text is displayed immediately.')
-            await client.connected()
-            await asyncio.sleep(2)
-            ui.label('This text is displayed 2 seconds after the page has been fully loaded.')
-            ui.label(f'The IP address {client.ip} was obtained from the websocket.')
-
-        ui.link('wait for connection', wait_for_connection)
-
-    @example('''#### Page Layout
-
-With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
-The `fixed` argument controls whether the element should scroll or stay fixed on the screen.
-The `top_corner` and `bottom_corner` arguments indicate whether a drawer should expand to the top or bottom of the page.
-See <https://quasar.dev/layout/header-and-footer> and <https://quasar.dev/layout/drawer> for more information about possible props.
-With `ui.page_sticky` you can place an element "sticky" on the screen.
-See <https://quasar.dev/layout/page-sticky> for more information.
-''', menu)
-    def page_layout_example():
-        @ui.page('/page_layout')
-        async def page_layout():
-            ui.label('CONTENT')
-            [ui.label(f'Line {i}') for i in range(100)]
-            with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
-                ui.label('HEADER')
-                ui.button(on_click=lambda: right_drawer.toggle()).props('flat color=white icon=menu')
-            with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4'):
-                ui.label('LEFT DRAWER')
-            with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as right_drawer:
-                ui.label('RIGHT DRAWER')
-            with ui.footer().style('background-color: #3874c8'):
-                ui.label('FOOTER')
-
-        ui.link('show page with fancy layout', page_layout)
-
-    @example(ui.open, menu)
-    def ui_open_example():
-        @ui.page('/yet_another_page')
-        def yet_another_page():
-            ui.label('Welcome to yet another page')
-            ui.button('RETURN', on_click=lambda: ui.open('reference#open'))
-
-        ui.button('REDIRECT', on_click=lambda: ui.open(yet_another_page))
-
-    @example('''#### Sessions
-
-The optional `request` argument provides insights about the client's URL parameters etc.
-It also enables you to identify sessions using a [session middleware](https://www.starlette.io/middleware/#sessionmiddleware).
-''', menu)
-    def sessions_example():
-        import uuid
-        from collections import Counter
-        from datetime import datetime
-
-        from starlette.middleware.sessions import SessionMiddleware
-        from starlette.requests import Request
-
-        from nicegui import app
-
-        # app.add_middleware(SessionMiddleware, secret_key='some_random_string')
-
-        counter = Counter()
-        start = datetime.now().strftime('%H:%M, %d %B %Y')
-
-        @ui.page('/session_demo')
-        def session_demo(request: Request):
-            if 'id' not in request.session:
-                request.session['id'] = str(uuid.uuid4())
-            counter[request.session['id']] += 1
-            ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
-
-        ui.link('Visit session demo', session_demo)
-
-    @example(ui.run_javascript, menu)
-    def javascript_example():
-        async def alert():
-            await ui.run_javascript('alert("Hello!")', respond=False)
-
-        async def get_date():
-            time = await ui.run_javascript('Date()')
-            ui.notify(f'Browser time: {time}')
-
-        async def access_elements():
-            await ui.run_javascript(f'getElement({label.id}).innerText += " Hello!"')
-
-        ui.button('fire and forget', on_click=alert)
-        ui.button('receive result', on_click=get_date)
-        ui.button('access elements', on_click=access_elements)
-        label = ui.label()
-
-    h3('Routes')
-
-    @example(app.add_static_files, menu)
-    def add_static_files_example():
-        from nicegui import app
-
-        app.add_static_files('/examples', 'examples')
-        ui.label('Some NiceGUI Examples').classes('text-h5')
-        ui.link('AI interface', '/examples/ai_interface/main.py')
-        ui.link('Custom FastAPI app', '/examples/fastapi/main.py')
-        ui.link('Authentication', '/examples/authentication/main.py')
-
-    @example('''#### API Responses
-
-NiceGUI is based on [FastAPI](https://fastapi.tiangolo.com/).
-This means you can use all of FastAPI's features.
-For example, you can implement a RESTful API in addition to your graphical user interface.
-You simply import the `app` object from `nicegui`.
-Or you can run NiceGUI on top of your own FastAPI app by using `ui.run_with(app)` instead of starting a server automatically with `ui.run()`.
-
-You can also return any other FastAPI response object inside a page function.
-For example, you can return a `RedirectResponse` to redirect the user to another page if certain conditions are met.
-This is used in our [authentication demo](https://github.com/zauberzeug/nicegui/tree/main/examples/authentication/main.py).
-''', menu)
-    def fastapi_example():
-        import random
-
-        from nicegui import app
-
-        @app.get('/random/{max}')
-        def generate_random_number(max: int):
-            return {'min': 0, 'max': max, 'value': random.randint(0, max)}
-
-        max = ui.number('max', value=100)
-        ui.button('generate random number', on_click=lambda: ui.open(f'/random/{max.value:.0f}'))
-
-    h3('Lifecycle')
-
-    @example('''#### Events
-
-You can register coroutines or functions to be called for the following events:
-
-- `app.on_startup`: called when NiceGUI is started or restarted
-- `app.on_shutdown`: called when NiceGUI is shut down or restarted
-- `app.on_connect`: called for each client which connects (optional argument: nicegui.Client)
-- `app.on_disconnect`: called for each client which disconnects (optional argument: nicegui.Client)
-- `app.on_exception`: called when an exception occurs (optional argument: exception)
-
-When NiceGUI is shut down or restarted, all tasks still in execution will be automatically canceled.
-''', menu)
-    def lifecycle_example():
-        from datetime import datetime
-
-        from nicegui import app
-
-        # dt = datetime.now()
-
-        def handle_connection():
-            global dt
-            dt = datetime.now()
-        app.on_connect(handle_connection)
-
-        label = ui.label()
-        ui.timer(1, lambda: label.set_text(f'Last new connection: {dt:%H:%M:%S}'))
-        # END OF EXAMPLE
-        global dt
-        dt = datetime.now()
-
-    @example(app.shutdown, menu)
-    def shutdown_example():
-        from nicegui import app
-
-        # ui.button('shutdown', on_click=app.shutdown)
-        #
-        # ui.run(reload=False)
-        # END OF EXAMPLE
-        ui.button('shutdown', on_click=lambda: ui.notify(
-            'Nah. We do not actually shutdown the documentation server. Try it in your own app!'))
-
-    h3('NiceGUI Fundamentals')
-
-    @example('''#### Auto-context
-
-In order to allow writing intuitive UI descriptions, NiceGUI automatically tracks the context in which elements are created.
-This means that there is no explicit `parent` parameter.
-Instead the parent context is defined using a `with` statement.
-It is also passed to event handlers and timers.
-
-In the example, the label "Card content" is added to the card.
-And because the `ui.button` is also added to the card, the label "Click!" will also be created in this context.
-The label "Tick!", which is added once after one second, is also added to the card.
-
-This design decision allows for easily creating modular components that keep working after moving them around in the UI.
-For example, you can move label and button somewhere else, maybe wrap them in another container, and the code will still work.
-''', menu)
-    def auto_context_example():
-        with ui.card():
-            ui.label('Card content')
-            ui.button('Add label', on_click=lambda: ui.label('Click!'))
-            ui.timer(1.0, lambda: ui.label('Tick!'), once=True)
-
-    @example('''#### Generic Events
-
-Most UI elements come with predefined events.
-For example, a `ui.button` like "A" in the example has an `on_click` parameter that expects a coroutine or function.
-But you can also use the `on` method to register a generic event handler like for "B".
-This allows you to register handlers for any event that is supported by JavaScript and Quasar.
-
-For example, you can register a handler for the `mousemove` event like for "C", even though there is no `on_mousemove` parameter for `ui.button`.
-Some events, like `mousemove`, are fired very often.
-To avoid performance issues, you can use the `throttle` parameter to only call the handler every `throttle` seconds ("D").
-
-The generic event handler can be synchronous or asynchronous and optionally takes an event dictionary as argument ("E").
-You can also specify which attributes of the JavaScript or Quasar event should be passed to the handler ("F").
-This can reduce the amount of data that needs to be transferred between the server and the client.
-
-You can also include [key modifiers](https://vuejs.org/guide/essentials/event-handling.html#key-modifiers) ("G"),
-modifier combinations ("H"),
-and [event modifiers](https://vuejs.org/guide/essentials/event-handling.html#mouse-button-modifiers) ("I").
-    ''', menu)
-    def generic_events_example():
-        with ui.row():
-            ui.button('A', on_click=lambda: ui.notify('You clicked the button A.'))
-            ui.button('B').on('click', lambda: ui.notify('You clicked the button B.'))
-        with ui.row():
-            ui.button('C').on('mousemove', lambda: ui.notify('You moved on button C.'))
-            ui.button('D').on('mousemove', lambda: ui.notify('You moved on button D.'), throttle=0.5)
-        with ui.row():
-            ui.button('E').on('mousedown', lambda e: ui.notify(str(e)))
-            ui.button('F').on('mousedown', lambda e: ui.notify(str(e)), ['ctrlKey', 'shiftKey'])
-        with ui.row():
-            ui.input('G').classes('w-12').on('keydown.space', lambda: ui.notify('You pressed space.'))
-            ui.input('H').classes('w-12').on('keydown.y.shift', lambda: ui.notify('You pressed Shift+Y'))
-            ui.input('I').classes('w-12').on('keydown.once', lambda: ui.notify('You started typing.'))
-    h3('Configuration')
-
-    @example(ui.run, menu, browser_title='My App')
-    def ui_run_example():
-        ui.label('page with custom title')
-
-        # ui.run(title='My App')
-
-    @example('''#### Environment Variables
-
-You can set the following environment variables to configure NiceGUI:
-
-- `MATPLOTLIB` (default: true) can be set to `false` to avoid the potentially costly import of Matplotlib. This will make `ui.pyplot` and `ui.line_plot` unavailable.
-- `MARKDOWN_CONTENT_CACHE_SIZE` (default: 1000): The maximum number of Markdown content snippets that are cached in memory.
-''', menu)
-    def env_var_example():
-        from nicegui.elements import markdown
-
-        ui.label(f'Markdown content cache size is {markdown.prepare_content.cache_info().maxsize}')
-
-    h3('Deployment')
-
-    with ui.column().classes('w-full mb-8 bold-links arrow-links'):
-        add_markdown_with_headline('''#### Server Hosting
-
-To deploy your NiceGUI app on a server, you will need to execute your `main.py` (or whichever file contains your `ui.run(...)`) on your cloud infrastructure.
-You can, for example, just install the [NiceGUI python package via pip](https://pypi.org/project/nicegui/) and use systemd or similar service to start the main script.
-In most cases, you will set the port to 80 (or 443 if you want to use HTTPS) with the `ui.run` command to make it easily accessible from the outside.
-
-A convenient alternative is the use of our [pre-built multi-arch Docker image](https://hub.docker.com/r/zauberzeug/nicegui) which contains all necessary dependencies.
-With this command you can launch the script `main.py` in the current directory on the public port 80:
-''')
-
-        with bash_window(classes='max-w-lg w-full h-52'):
-            ui.markdown('```bash\ndocker run -p 80:8080 -v $(pwd)/:/app/ \\\n    -d --restart always zauberzeug/nicegui:latest\n```')
-
-        ui.markdown(
-            '''The example assumes `main.py` uses the port 8080 in the `ui.run` command (which is the default).
-The `-d` tells docker to run in background and `--restart always` makes sure the container is restarted if the app crashes or the server reboots.
-Of course this can also be written in a Docker compose file:
-''')
-        with python_window('docker-compose.yml', classes='max-w-lg w-full h-52'):
-            ui.markdown('''```yaml
-app:
-    image: zauberzeug/nicegui:latest
-    restart: always
-    ports:
-        - 80:8080
-    volumes:
-        - ./:/app/
-```
-            ''')
-
-        ui.markdown('''
-You can provide SSL certificates directly using [FastAPI](https://fastapi.tiangolo.com/deployment/https/).
-In production we also like using reverse proxies like [Traefik](https://doc.traefik.io/traefik/) or [NGINX](https://www.nginx.com/) to handle these details for us.
-See our [docker-compose.yml](https://github.com/zauberzeug/nicegui/blob/main/docker-compose.yml) as an example.
-
-You may also have a look at [our demo for using a custom FastAPI app](https://github.com/zauberzeug/nicegui/tree/main/examples/fastapi).
-This will allow you to do very flexible deployments as described in the [FastAPI documentation](https://fastapi.tiangolo.com/deployment/).
-Note that there are additional steps required to allow multiple workers.
-''')
-
-        with ui.column().classes('w-full mt-8 arrow-links'):
-            add_markdown_with_headline('''#### Package for Installation
-
-NiceGUI apps can also be bundled into an executable with [PyInstaller](https://www.pyinstaller.org/).
-This allows you to distribute your app as a single file that can be executed on any computer.
-
-Just take care your `ui.run` command does not use the `reload` argument.
-Running the `build.py` below will create an executable `myapp` in the `dist` folder:
-''')
-
-        with ui.row().classes('w-full items-stretch'):
-            with python_window(classes='max-w-lg w-full'):
-                ui.markdown('''```python
-from nicegui import ui
-
-ui.label('Hello from Pyinstaller')
-
-ui.run(reload=False)
-```''')
-            with python_window('build.py', classes='max-w-lg w-full'):
-                ui.markdown('''```python
-import os
-import subprocess
-from pathlib import Path
-import nicegui
-
-cmd = [
-    'pyinstaller',
-    'main.py', # your main file with ui.run()
-    '--name', 'myapp', # name of your app
-    '--onefile',
-    '--add-data', f'{Path(nicegui.__file__).parent}{os.pathsep}nicegui'       
-]
-subprocess.call(cmd)
-        ```''')
-
-        ui.markdown('''
-
-        ''')

+ 7 - 7
website/static/header.html

@@ -4,14 +4,14 @@
 />
 
 <!-- https://realfavicongenerator.net/ -->
-<link rel="apple-touch-icon" sizes="180x180" href="favicon/apple-touch-icon.png" />
-<link rel="icon" type="image/png" sizes="32x32" href="favicon/favicon-32x32.png" />
-<link rel="icon" type="image/png" sizes="16x16" href="favicon/favicon-16x16.png" />
-<link rel="manifest" href="favicon/site.webmanifest" />
-<link rel="mask-icon" href="favicon/safari-pinned-tab.svg" color="#5898d4" />
-<link rel="shortcut icon" href="favicon/favicon.ico" />
+<link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
+<link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
+<link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
+<link rel="manifest" href="/favicon/site.webmanifest" />
+<link rel="mask-icon" href="/favicon/safari-pinned-tab.svg" color="#5898d4" />
+<link rel="shortcut icon" href="/favicon/favicon.ico" />
 <meta name="msapplication-TileColor" content="#2b5797" />
-<meta name="msapplication-config" content="favicon/browserconfig.xml" />
+<meta name="msapplication-config" content="/favicon/browserconfig.xml" />
 <meta name="theme-color" content="#5898d4" />
 
 <script>

+ 8 - 4
website/static/style.css

@@ -84,20 +84,24 @@ a:active:not(.browser-window *) {
   }
 }
 
-dl.field-list {
+dl.field-list,
+dl.docinfo {
   display: grid;
   grid-template-columns: max-content auto;
   margin: 1em 0;
 }
-dl.field-list dt {
+dl.field-list dt,
+dl.docinfo dt {
   grid-column-start: 1;
   margin-right: 1em;
   font-weight: 500;
 }
-dl.field-list dd {
+dl.field-list dd,
+dl.docinfo dd {
   grid-column-start: 2;
 }
-dl.field-list p {
+dl.field-list p,
+dl.docinfo p {
   margin-bottom: 0;
 }
 

+ 6 - 0
website/style.py

@@ -45,3 +45,9 @@ def features(icon: str, title: str, items: List[str]) -> None:
         ui.label(title).classes('font-bold mb-3')
         for item in items:
             ui.markdown(f'- {item}').classes('bold-links arrow-links')
+
+
+def side_menu() -> ui.left_drawer:
+    return ui.left_drawer() \
+        .classes('column no-wrap gap-1 bg-[#eee] mt-[-20px] px-8 py-20') \
+        .style('height: calc(100% + 20px) !important')