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

Merge branch 'main' into notification

Falko Schindler пре 1 година
родитељ
комит
764e04b78c
100 измењених фајлова са 3336 додато и 792 уклоњено
  1. 3 3
      CITATION.cff
  2. 3 2
      CONTRIBUTING.md
  3. 1 1
      fetch_milestone.py
  4. 18 428
      main.py
  5. 7 3
      nicegui/air.py
  6. 3 0
      nicegui/app/app_config.py
  7. 19 5
      nicegui/client.py
  8. 1 1
      nicegui/elements/chat_message.py
  9. 18 3
      nicegui/elements/highchart.py
  10. 11 6
      nicegui/elements/image.py
  11. 10 5
      nicegui/elements/interactive_image.py
  12. 31 6
      nicegui/functions/html.py
  13. 9 3
      nicegui/functions/open.py
  14. 2 2
      nicegui/native/native_mode.py
  15. 5 4
      nicegui/nicegui.py
  16. 12 3
      nicegui/optional_features.py
  17. 10 1
      nicegui/page.py
  18. 11 6
      nicegui/ui_run.py
  19. 3 2
      nicegui/ui_run_with.py
  20. 7 5
      nicegui/welcome.py
  21. 9 4
      tests/screen.py
  22. 13 0
      tests/test_page.py
  23. 4 15
      website/__init__.py
  24. 13 0
      website/anti_scroll_hack.py
  25. 0 160
      website/build_search_index.py
  26. 8 10
      website/documentation/__init__.py
  27. 0 110
      website/documentation/content.py
  28. 7 0
      website/documentation/content/__init__.py
  29. 4 1
      website/documentation/content/add_static_files_documentation.py
  30. 198 0
      website/documentation/content/aggrid_documentation.py
  31. 25 0
      website/documentation/content/audio_documentation.py
  32. 20 0
      website/documentation/content/avatar_documentation.py
  33. 6 0
      website/documentation/content/badge_documentation.py
  34. 64 0
      website/documentation/content/button_documentation.py
  35. 53 0
      website/documentation/content/card_documentation.py
  36. 6 0
      website/documentation/content/carousel_documentation.py
  37. 46 0
      website/documentation/content/chat_message_documentation.py
  38. 6 0
      website/documentation/content/checkbox_documentation.py
  39. 26 0
      website/documentation/content/circular_progress_documentation.py
  40. 6 0
      website/documentation/content/code_documentation.py
  41. 6 0
      website/documentation/content/color_input_documentation.py
  42. 6 0
      website/documentation/content/color_picker_documentation.py
  43. 8 2
      website/documentation/content/colors_documentation.py
  44. 26 0
      website/documentation/content/column_documentation.py
  45. 6 0
      website/documentation/content/context_menu_documentation.py
  46. 6 1
      website/documentation/content/dark_mode_documentation.py
  47. 37 0
      website/documentation/content/date_documentation.py
  48. 51 0
      website/documentation/content/dialog_documentation.py
  49. 13 0
      website/documentation/content/doc/__init__.py
  50. 150 0
      website/documentation/content/doc/api.py
  51. 21 0
      website/documentation/content/doc/page.py
  52. 30 0
      website/documentation/content/doc/part.py
  53. 15 0
      website/documentation/content/download_documentation.py
  54. 50 0
      website/documentation/content/echart_documentation.py
  55. 6 0
      website/documentation/content/editor_documentation.py
  56. 73 0
      website/documentation/content/element_documentation.py
  57. 22 0
      website/documentation/content/expansion_documentation.py
  58. 124 0
      website/documentation/content/generic_events_documentation.py
  59. 6 0
      website/documentation/content/grid_documentation.py
  60. 76 0
      website/documentation/content/highchart_documentation.py
  61. 6 0
      website/documentation/content/html_documentation.py
  62. 30 0
      website/documentation/content/icon_documentation.py
  63. 65 0
      website/documentation/content/image_documentation.py
  64. 45 0
      website/documentation/content/input_documentation.py
  65. 41 0
      website/documentation/content/interactive_image_documentation.py
  66. 6 0
      website/documentation/content/joystick_documentation.py
  67. 6 0
      website/documentation/content/json_editor_documentation.py
  68. 6 0
      website/documentation/content/keyboard_documentation.py
  69. 6 0
      website/documentation/content/knob_documentation.py
  70. 29 0
      website/documentation/content/label_documentation.py
  71. 6 0
      website/documentation/content/line_plot_documentation.py
  72. 6 0
      website/documentation/content/linear_progress_documentation.py
  73. 59 0
      website/documentation/content/link_documentation.py
  74. 42 0
      website/documentation/content/log_documentation.py
  75. 58 0
      website/documentation/content/markdown_documentation.py
  76. 6 0
      website/documentation/content/menu_documentation.py
  77. 6 0
      website/documentation/content/mermaid_documentation.py
  78. 33 0
      website/documentation/content/notify_documentation.py
  79. 33 0
      website/documentation/content/number_documentation.py
  80. 9 0
      website/documentation/content/open_documentation.py
  81. 119 0
      website/documentation/content/overview.py
  82. 86 0
      website/documentation/content/page_documentation.py
  83. 6 0
      website/documentation/content/pagination_documentation.py
  84. 70 0
      website/documentation/content/plotly_documentation.py
  85. 6 0
      website/documentation/content/pyplot_documentation.py
  86. 38 0
      website/documentation/content/query_documentation.py
  87. 6 0
      website/documentation/content/radio_documentation.py
  88. 94 0
      website/documentation/content/refreshable_documentation.py
  89. 6 0
      website/documentation/content/row_documentation.py
  90. 82 0
      website/documentation/content/run_documentation.py
  91. 51 0
      website/documentation/content/run_javascript_documentation.py
  92. 102 0
      website/documentation/content/scene_documentation.py
  93. 48 0
      website/documentation/content/scroll_area_documentation.py
  94. 152 0
      website/documentation/content/section_action_events.py
  95. 47 0
      website/documentation/content/section_audiovisual_elements.py
  96. 70 0
      website/documentation/content/section_binding_properties.py
  97. 283 0
      website/documentation/content/section_configuration_deployment.py
  98. 26 0
      website/documentation/content/section_controls.py
  99. 28 0
      website/documentation/content/section_data_elements.py
  100. 88 0
      website/documentation/content/section_page_layout.py

+ 3 - 3
CITATION.cff

@@ -8,7 +8,7 @@ authors:
   given-names: Rodja
   orcid: https://orcid.org/0009-0009-4735-6227
 title: 'NiceGUI: Web-based user interfaces with Python. The nice way.'
-version: v1.4.2
-date-released: '2023-11-06'
+version: v1.4.4
+date-released: '2023-12-04'
 url: https://github.com/zauberzeug/nicegui
-doi: 10.5281/zenodo.10075791
+doi: 10.5281/zenodo.10256862

+ 3 - 2
CONTRIBUTING.md

@@ -110,9 +110,10 @@ If you plan to implement a new element you can follow these suggestions:
 5. Look at other similar elements and how they are implemented in `nicegui/elements`.
 6. Create a new file with your new element alongside the existing ones.
 7. Make sure your element works as expected.
-8. Add documentation 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.
+8. Add a documentation file in `website/documentation/content`.
+   By calling the `@doc.demo(...)` function with an element as a parameter the docstring is used as a description.
    The docstrings are written in restructured-text.
+   Refer to the new documentation page using `@doc.intro(...)` in any documentation section `website/documentation/content/section_*.py`.
 9. Create a pull-request (see below).
 
 ### Additional Demos

+ 1 - 1
fetch_milestone.py

@@ -37,7 +37,7 @@ notes: Dict[str, List[str]] = {
 for issue in issues:
     title: str = issue['title']
     user: str = issue['user']['login']
-    body: str = issue['body']
+    body: str = issue['body'] or ''
     labels: list[str] = [label['name'] for label in issue['labels']]
     number_patterns = [r'#(\d+)', r'https://github.com/zauberzeug/nicegui/(?:issues|discussions|pulls)/(\d+)']
     numbers = [issue['number']] + [int(match) for pattern in number_patterns for match in re.findall(pattern, body)]

+ 18 - 428
main.py

@@ -1,465 +1,55 @@
 #!/usr/bin/env python3
-import importlib
-import inspect
-import logging
 import os
 from pathlib import Path
-from typing import Awaitable, Callable, Optional
-from urllib.parse import parse_qs
 
 from fastapi import Request
-from fastapi.responses import FileResponse, RedirectResponse, Response
-from starlette.middleware.base import BaseHTTPMiddleware
 from starlette.middleware.sessions import SessionMiddleware
-from starlette.types import ASGIApp, Receive, Scope, Send
 
 import prometheus
-from nicegui import Client, app, ui
-from website import (Search, add_star, documentation, example_card, example_link, features, heading, link_target,
-                     section_heading, side_menu, subtitle, svg, title)
+from nicegui import app, ui
+from website import anti_scroll_hack, documentation, fly, main_page, svg
 
 prometheus.start_monitor(app)
 
 # session middleware is required for demo in documentation and prometheus
 app.add_middleware(SessionMiddleware, secret_key=os.environ.get('NICEGUI_SECRET_KEY', ''))
 
+on_fly = fly.setup()
+anti_scroll_hack.setup()
+
 app.add_static_files('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
 app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 app.add_static_files('/static', str(Path(__file__).parent / 'website' / 'static'))
+app.add_static_file(local_file=svg.PATH / 'logo.png', url_path='/logo.png')
+app.add_static_file(local_file=svg.PATH / 'logo_square.png', url_path='/logo_square.png')
 
-if True:  # HACK: prevent the page from scrolling when closing a dialog (#1404)
-    def _handle_value_change(sender, value, on_value_change=ui.dialog._handle_value_change) -> None:
-        ui.query('html').classes(**{'add' if value else 'remove': 'has-dialog'})
-        on_value_change(sender, value)
-    ui.dialog._handle_value_change = _handle_value_change
-
-
-@app.get('/logo.png')
-def logo() -> FileResponse:
-    return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
-
-
-@app.get('/logo_square.png')
-def logo_square() -> FileResponse:
-    return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
+documentation.build_search_index()
 
 
 @app.post('/dark_mode')
-async def post_dark_mode(request: Request) -> None:
+async def _post_dark_mode(request: Request) -> None:
     app.storage.browser['dark_mode'] = (await request.json()).get('value')
 
 
-@app.middleware('http')
-async def redirect_reference_to_documentation(request: Request,
-                                              call_next: Callable[[Request], Awaitable[Response]]) -> Response:
-    if request.url.path == '/reference':
-        return RedirectResponse('/documentation')
-    try:
-        return await call_next(request)
-    except RuntimeError as e:
-        logging.error(f'Error while processing request {request.url.path}: {e}')
-
-# NOTE In our global fly.io deployment we need to make sure that we connect back to the same instance.
-fly_instance_id = os.environ.get('FLY_ALLOC_ID', 'local').split('-')[0]
-app.config.socket_io_js_extra_headers['fly-force-instance-id'] = fly_instance_id  # for HTTP long polling
-app.config.socket_io_js_query_params['fly_instance_id'] = fly_instance_id  # for websocket (FlyReplayMiddleware)
-
-
-class FlyReplayMiddleware(BaseHTTPMiddleware):
-    """Replay to correct fly.io instance.
-
-    If the wrong instance was picked by the fly.io load balancer, we use the fly-replay header
-    to repeat the request again on the right instance.
-
-    This only works if the correct instance is provided as a query_string parameter.
-    """
-
-    def __init__(self, app: ASGIApp) -> None:
-        self.app = app
-        self.app_name = os.environ.get('FLY_APP_NAME')
-
-    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
-        query_string = scope.get('query_string', b'').decode()
-        query_params = parse_qs(query_string)
-        target_instance = query_params.get('fly_instance_id', [fly_instance_id])[0]
-
-        async def send_wrapper(message):
-            if target_instance != fly_instance_id and self.is_online(target_instance):
-                if message['type'] == 'websocket.close':
-                    # fly.io only seems to look at the fly-replay header if websocket is accepted
-                    message = {'type': 'websocket.accept'}
-                if 'headers' not in message:
-                    message['headers'] = []
-                message['headers'].append([b'fly-replay', f'instance={target_instance}'.encode()])
-            await send(message)
-        await self.app(scope, receive, send_wrapper)
-
-    def is_online(self, fly_instance_id: str) -> bool:
-        hostname = f'{fly_instance_id}.vm.{self.app_name}.internal'
-        try:
-            dns.resolver.resolve(hostname, 'AAAA')
-            return True
-        except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN, dns.resolver.NoNameservers, dns.resolver.Timeout):
-            return False
-
-
-if 'FLY_ALLOC_ID' in os.environ:
-    import dns.resolver  # NOTE only import on fly where we have it installed to look up if instance is still available
-    app.add_middleware(FlyReplayMiddleware)
-
-
-def add_head_html() -> None:
-    ui.add_head_html((Path(__file__).parent / 'website' / 'static' / 'header.html').read_text())
-    ui.add_head_html(f"<style>{(Path(__file__).parent / 'website' / 'static' / 'style.css').read_text()}</style>")
-
-
-def add_header(menu: Optional[ui.left_drawer] = None) -> None:
-    menu_items = {
-        'Installation': '/#installation',
-        'Features': '/#features',
-        'Demos': '/#demos',
-        'Documentation': '/documentation',
-        'Examples': '/#examples',
-        'Why?': '/#why',
-    }
-    dark_mode = ui.dark_mode(value=app.storage.browser.get('dark_mode'), on_change=lambda e: ui.run_javascript(f'''
-        fetch('/dark_mode', {{
-            method: 'POST',
-            headers: {{'Content-Type': 'application/json'}},
-            body: JSON.stringify({{value: {e.value}}}),
-        }});
-    '''))
-    with ui.header() \
-            .classes('items-center duration-200 p-0 px-4 no-wrap') \
-            .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
-        if menu:
-            ui.button(on_click=menu.toggle, icon='menu').props('flat color=white round').classes('lg:hidden')
-        with ui.link(target=index_page).classes('row gap-4 items-center no-wrap mr-auto'):
-            svg.face().classes('w-8 stroke-white stroke-2 max-[550px]:hidden')
-            svg.word().classes('w-24')
-
-        with ui.row().classes('max-[1050px]:hidden'):
-            for title_, target in menu_items.items():
-                ui.link(title_, target).classes(replace='text-lg text-white')
-
-        search = Search()
-        search.create_button()
-
-        with ui.element().classes('max-[360px]:hidden'):
-            ui.button(icon='dark_mode', on_click=lambda: dark_mode.set_value(None)) \
-                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', value=True)
-            ui.button(icon='light_mode', on_click=lambda: dark_mode.set_value(True)) \
-                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', value=False)
-            ui.button(icon='brightness_auto', on_click=lambda: dark_mode.set_value(False)) \
-                .props('flat fab-mini color=white').bind_visibility_from(dark_mode, 'value', lambda mode: mode is None)
-
-        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[455px]:hidden').tooltip('Discord'):
-            svg.discord().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[405px]:hidden').tooltip('Reddit'):
-            svg.reddit().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://github.com/zauberzeug/nicegui/').classes('max-[305px]:hidden').tooltip('GitHub'):
-            svg.github().classes('fill-white scale-125 m-1')
-
-        add_star().classes('max-[490px]:hidden')
-
-        with ui.row().classes('min-[1051px]:hidden'):
-            with ui.button(icon='more_vert').props('flat color=white round'):
-                with ui.menu().classes('bg-primary text-white text-lg'):
-                    for title_, target in menu_items.items():
-                        ui.menu_item(title_, on_click=lambda target=target: ui.open(target))
-
-
 @ui.page('/')
-async def index_page(client: Client) -> None:
-    client.content.classes('p-0 gap-0')
-    add_head_html()
-    add_header()
-
-    with ui.row().classes('w-full h-screen items-center gap-8 pr-4 no-wrap into-section'):
-        svg.face(half=True).classes('stroke-black dark:stroke-white w-[200px] md:w-[230px] lg:w-[300px]')
-        with ui.column().classes('gap-4 md:gap-8 pt-32'):
-            title('Meet the *NiceGUI*.')
-            subtitle('And let any browser be the frontend of your Python code.') \
-                .classes('max-w-[20rem] sm:max-w-[24rem] md:max-w-[30rem]')
-            ui.link(target='#about').classes('scroll-indicator')
-
-    with ui.row().classes('''
-            dark-box min-h-screen no-wrap
-            justify-center items-center flex-col md:flex-row
-            py-20 px-8 lg:px-16
-            gap-8 sm:gap-16 md:gap-8 lg:gap-16
-        '''):
-        link_target('about')
-        with ui.column().classes('text-white max-w-4xl'):
-            heading('Interact with Python through buttons, dialogs, 3D&nbsp;scenes, plots and much more.')
-            with ui.column().classes('gap-2 bold-links arrow-links text-lg'):
-                ui.markdown('''
-                    NiceGUI manages web development details, letting you focus on Python code for diverse applications,
-                    including robotics, IoT solutions, smart home automation, and machine learning.
-                    Designed to work smoothly with connected peripherals like webcams and GPIO pins in IoT setups,
-                    NiceGUI streamlines the management of all your code in one place.
-                    <br><br>
-                    With a gentle learning curve, NiceGUI is user-friendly for beginners
-                    and offers advanced customization for experienced users,
-                    ensuring simplicity for basic tasks and feasibility for complex projects.
-                    <br><br><br>
-                    Available as
-                    [PyPI package](https://pypi.org/project/nicegui/),
-                    [Docker image](https://hub.docker.com/r/zauberzeug/nicegui) and on
-                    [GitHub](https://github.com/zauberzeug/nicegui).
-                ''')
-        example_card.create()
-
-    with ui.column().classes('w-full text-lg p-8 lg:p-16 max-w-[1600px] mx-auto'):
-        link_target('installation', '-50px')
-        section_heading('Installation', 'Get *started*')
-        with ui.row().classes('w-full text-lg leading-tight grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-8'):
-            with ui.column().classes('w-full max-w-md gap-2'):
-                ui.html('<em>1.</em>').classes('text-3xl font-bold')
-                ui.markdown('Create __main.py__').classes('text-lg')
-                with documentation.python_window(classes='w-full h-52'):
-                    ui.markdown('''
-                        ```python\n
-                        from nicegui import ui
-
-                        ui.label('Hello NiceGUI!')
-
-                        ui.run()
-                        ```
-                    ''')
-            with ui.column().classes('w-full max-w-md gap-2'):
-                ui.html('<em>2.</em>').classes('text-3xl font-bold')
-                ui.markdown('Install and launch').classes('text-lg')
-                with documentation.bash_window(classes='w-full h-52'):
-                    ui.markdown('''
-                        ```bash
-                        pip3 install nicegui
-                        python3 main.py
-                        ```
-                    ''')
-            with ui.column().classes('w-full max-w-md gap-2'):
-                ui.html('<em>3.</em>').classes('text-3xl font-bold')
-                ui.markdown('Enjoy!').classes('text-lg')
-                with documentation.browser_window(classes='w-full h-52'):
-                    ui.label('Hello NiceGUI!')
-        with ui.expansion('...or use Docker to run your main.py').classes('w-full gap-2 bold-links arrow-links'):
-            with ui.row().classes('mt-8 w-full justify-center items-center gap-8'):
-                ui.markdown('''
-                    With our [multi-arch Docker image](https://hub.docker.com/repository/docker/zauberzeug/nicegui) 
-                    you can start the server without installing any packages.
-
-                    The command searches for `main.py` in in your current directory and makes the app available at http://localhost:8888.
-                ''').classes('max-w-xl')
-                with documentation.bash_window(classes='max-w-lg w-full h-52'):
-                    ui.markdown('''
-                        ```bash
-                        docker run -it --rm -p 8888:8080 \\
-                            -v "$PWD":/app zauberzeug/nicegui
-                        ```
-                    ''')
-
-    with ui.column().classes('w-full p-8 lg:p-16 bold-links arrow-links max-w-[1600px] mx-auto'):
-        link_target('features', '-50px')
-        section_heading('Features', 'Code *nicely*')
-        with ui.row().classes('w-full text-lg leading-tight grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-8'):
-            features('swap_horiz', 'Interaction', [
-                'buttons, switches, sliders, inputs, ...',
-                'notifications, dialogs and menus',
-                'interactive images with SVG overlays',
-                'web pages and native window apps',
-            ])
-            features('space_dashboard', 'Layout', [
-                'navigation bars, tabs, panels, ...',
-                'grouping with rows, columns, grids and cards',
-                'HTML and Markdown elements',
-                'flex layout by default',
-            ])
-            features('insights', 'Visualization', [
-                'charts, diagrams and tables',
-                '3D scenes',
-                'straight-forward data binding',
-                'built-in timer for data refresh',
-            ])
-            features('brush', 'Styling', [
-                'customizable color themes',
-                'custom CSS and classes',
-                'modern look with material design',
-                '[Tailwind CSS](https://tailwindcss.com/) auto-completion',
-            ])
-            features('source', 'Coding', [
-                'routing for multiple pages',
-                'auto-reload on code change',
-                'persistent user sessions',
-                'Jupyter notebook compatibility',
-            ])
-            features('anchor', 'Foundation', [
-                'generic [Vue](https://vuejs.org/) to Python bridge',
-                'dynamic GUI through [Quasar](https://quasar.dev/)',
-                'content is served with [FastAPI](http://fastapi.tiangolo.com/)',
-                'Python 3.8+',
-            ])
-
-    with ui.column().classes('w-full p-8 lg:p-16 max-w-[1600px] mx-auto'):
-        link_target('demos', '-50px')
-        section_heading('Demos', 'Try *this*')
-        with ui.column().classes('w-full'):
-            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 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('Documentation', '/documentation').style('color: black !important') \
-                .classes('rounded-full mx-auto px-12 py-2 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('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')
-            example_link('Modularization',
-                         'provides an example of how to modularize your application into multiple files and reuse code')
-            example_link('FastAPI', 'illustrates the integration of NiceGUI with an existing FastAPI application')
-            example_link('Map',
-                         'demonstrates wrapping the JavaScript library [leaflet](https://leafletjs.com/) '
-                         'to display a map at specific locations')
-            example_link('AI Interface',
-                         'utilizes the [replicate](https://replicate.com) library to perform voice-to-text '
-                         'transcription and generate images from prompts with Stable Diffusion')
-            example_link('3D Scene', 'creates a webGL view and loads an STL mesh illuminated with a spotlight')
-            example_link('Custom Vue Component', 'shows how to write and integrate a custom Vue component')
-            example_link('Image Mask Overlay', 'shows how to overlay an image with a mask')
-            example_link('Infinite Scroll', 'presents an infinitely scrolling image gallery')
-            example_link('OpenCV Webcam', 'uses OpenCV to capture images from a webcam')
-            example_link('SVG Clock', 'displays an analog clock by updating an SVG with `ui.timer`')
-            example_link('Progress', 'demonstrates a progress bar for heavy computations')
-            example_link('NGINX Subpath', 'shows the setup to serve an app behind a reverse proxy subpath')
-            example_link('Script Executor', 'executes scripts on selection and displays the output')
-            example_link('Local File Picker', 'demonstrates a dialog for selecting files locally on the server')
-            example_link('Search as you type', 'using public API of thecocktaildb.com to search for cocktails')
-            example_link('Menu and Tabs', 'uses Quasar to create foldable menu and tabs inside a header bar')
-            example_link('Todo list', 'shows a simple todo list with checkboxes and text input')
-            example_link('Trello Cards', 'shows Trello-like cards that can be dragged and dropped into columns')
-            example_link('Slots', 'shows how to use scoped slots to customize Quasar elements')
-            example_link('Table and slots', 'shows how to use component slots in a table')
-            example_link('Single Page App', 'navigate without reloading the page')
-            example_link('Chat App', 'a simple chat app')
-            example_link('Chat with AI', 'a simple chat app with AI')
-            example_link('SQLite Database', 'CRUD operations on a SQLite database with async-support through Tortoise ORM')
-            example_link('Pandas DataFrame', 'displays an editable [pandas](https://pandas.pydata.org) DataFrame')
-            example_link('Lightbox', 'A thumbnail gallery where each image can be clicked to enlarge')
-            example_link('ROS2', 'Using NiceGUI as web interface for a ROS2 robot')
-            example_link('Docker Image',
-                         'Demonstrate using the official '
-                         '[zauberzeug/nicegui](https://hub.docker.com/r/zauberzeug/nicegui) docker image')
-            example_link('Download Text as File', 'providing in-memory data like strings as file download')
-            example_link('Generate PDF', 'create SVG preview and PDF download from input form elements')
-            example_link('Custom Binding', 'create a custom binding for a label with a bindable background color')
-            example_link('Descope Auth', 'login form and user profile using [Descope](https://descope.com)')
-            example_link('Editable table', 'editable table allowing to add, edit, delete rows')
-            example_link('Editable AG Grid', 'editable AG Grid allowing to add, edit, delete rows')
-
-    with ui.row().classes('dark-box min-h-screen mt-16'):
-        link_target('why')
-        with ui.column().classes('''
-                max-w-[1600px] m-auto
-                py-20 px-8 lg:px-16
-                items-center justify-center no-wrap flex-col md:flex-row gap-16
-            '''):
-            with ui.column().classes('gap-8'):
-                heading('Why?')
-                with ui.column().classes('gap-2 text-xl text-white bold-links arrow-links'):
-                    ui.markdown(
-                        'We at '
-                        '[Zauberzeug](https://zauberzeug.com) '
-                        'like '
-                        '[Streamlit](https://streamlit.io/) '
-                        'but find it does '
-                        '[too much magic](https://github.com/zauberzeug/nicegui/issues/1#issuecomment-847413651) '
-                        'when it comes to state handling. '
-                        'In search for an alternative nice library to write simple graphical user interfaces in Python we discovered '
-                        '[JustPy](https://justpy.io/). '
-                        'Although we liked the approach, it is too "low-level HTML" for our daily usage. '
-                        'But it inspired us to use '
-                        '[Vue](https://vuejs.org/) '
-                        'and '
-                        '[Quasar](https://quasar.dev/) '
-                        'for the frontend.')
-                    ui.markdown(
-                        'We have built on top of '
-                        '[FastAPI](https://fastapi.tiangolo.com/), '
-                        'which itself is based on the ASGI framework '
-                        '[Starlette](https://www.starlette.io/) '
-                        'and the ASGI webserver '
-                        '[Uvicorn](https://www.uvicorn.org/) '
-                        'because of their great performance and ease of use.'
-                    )
-            svg.face().classes('stroke-white shrink-0 w-[200px] md:w-[300px] lg:w-[450px]')
+def _main_page() -> None:
+    main_page.create()
 
 
 @ui.page('/documentation')
-def documentation_page() -> None:
-    add_head_html()
-    add_header()
-    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('Reference, Demos and more', '*NiceGUI* Documentation')
-        documentation.create_overview()
-
-
-@ui.page('/documentation/section_{name}')
-def documentation_section(name: str) -> None:
-    add_head_html()
-    with side_menu() as menu:
-        ui.markdown('[← Overview](/documentation)').classes('bold-links')
-    add_header(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'):
-        documentation.create_section(name)
+def _documentation_page() -> None:
+    documentation.render_page(documentation.registry[''], with_menu=False)
 
 
 @ui.page('/documentation/{name}')
-async def documentation_page_more(name: str, client: Client) -> None:
-    if name in {'ag_grid', 'e_chart'}:
-        name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
-    module = importlib.import_module(f'website.documentation.more.{name}_documentation')
-    more = getattr(module, 'more', None)
-    api = getattr(ui, name, name)
-
-    add_head_html()
-    add_header()
-    with side_menu() as menu:
-        ui.markdown('[← Overview](/documentation)').classes('bold-links')  # TODO: back to section
-    with ui.column().classes('w-full p-8 lg:p-16 max-w-[1250px] mx-auto'):
-        section_heading('Documentation', f'ui.*{name}*' if hasattr(ui, name) else f'*{name.replace("_", " ").title()}*')
-        with menu:
-            ui.markdown('**Demos**' if more else '**Demo**').classes('mt-4')
-        documentation.element_demo(api)(getattr(module, 'main_demo'))
-        if more:
-            more()
-        if inspect.isclass(api):
-            with menu:
-                ui.markdown('**Reference**').classes('mt-4')
-            ui.markdown('## Reference').classes('mt-16')
-            documentation.generate_class_doc(api)
-    try:
-        await client.connected()
-        ui.run_javascript(f'document.title = "{name} • NiceGUI";')
-    except TimeoutError:
-        logging.warning(f'client did not connect for page /documentation/{name}')
+def _documentation_detail_page(name: str) -> None:
+    documentation.render_page(documentation.registry[name])
 
 
 @app.get('/status')
-def status():
-    """for health checks"""
+def _status():
     return 'Ok'
 
 
-ui.run(uvicorn_reload_includes='*.py, *.css, *.html',
-       # NOTE: do not reload when running on fly.io (see https://github.com/zauberzeug/nicegui/discussions/1720#discussioncomment-7288741)
-       reload='FLY_ALLOC_ID' not in os.environ,
-       reconnect_timeout=10.0)
+# NOTE: do not reload on fly.io (see https://github.com/zauberzeug/nicegui/discussions/1720#discussioncomment-7288741)
+ui.run(uvicorn_reload_includes='*.py, *.css, *.html', reload=not on_fly, reconnect_timeout=10.0)

+ 7 - 3
nicegui/air.py

@@ -1,5 +1,6 @@
 import asyncio
 import gzip
+import json
 import re
 from typing import Any, Dict, Optional
 
@@ -55,7 +56,8 @@ class Air:
         @self.relay.on('ready')
         def _handle_ready(data: Dict[str, Any]) -> None:
             core.app.urls.add(data['device_url'])
-            print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
+            if core.app.config.show_welcome_message:
+                print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
 
         @self.relay.on('error')
         def _handleerror(data: Dict[str, Any]) -> None:
@@ -85,8 +87,10 @@ class Air:
             if client_id not in Client.instances:
                 return
             client = Client.instances[client_id]
-            if isinstance(data['msg']['args'], dict) and 'socket_id' in data['msg']['args']:
-                data['msg']['args']['socket_id'] = client_id  # HACK: translate socket_id of ui.scene's init event
+            if data['msg']['args'] and data['msg']['args'][0].startswith('{"socket_id":'):
+                args = json.loads(data['msg']['args'][0])
+                args['socket_id'] = client_id  # HACK: translate socket_id of ui.scene's init event
+                data['msg']['args'][0] = json.dumps(args)
             client.handle_event(data['msg'])
 
         @self.relay.on('javascript_response')

+ 3 - 0
nicegui/app/app_config.py

@@ -34,6 +34,7 @@ class AppConfig:
     reconnect_timeout: float = field(init=False)
     tailwind: bool = field(init=False)
     prod_js: bool = field(init=False)
+    show_welcome_message: bool = field(init=False)
     _has_run_config: bool = False
 
     def add_run_config(self,
@@ -48,6 +49,7 @@ class AppConfig:
                        reconnect_timeout: float,
                        tailwind: bool,
                        prod_js: bool,
+                       show_welcome_message: bool,
                        ) -> None:
         """Add the run config to the app config."""
         self.reload = reload
@@ -60,6 +62,7 @@ class AppConfig:
         self.reconnect_timeout = reconnect_timeout
         self.tailwind = tailwind
         self.prod_js = prod_js
+        self.show_welcome_message = show_welcome_message
         self._has_run_config = True
 
     @property

+ 19 - 5
nicegui/client.py

@@ -12,9 +12,7 @@ from fastapi import Request
 from fastapi.responses import Response
 from fastapi.templating import Jinja2Templates
 
-from nicegui import json
-
-from . import background_tasks, binding, core, helpers, outbox
+from . import background_tasks, binding, core, helpers, json, outbox
 from .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .element import Element
@@ -38,6 +36,12 @@ class Client:
     auto_index_client: Client
     """The client that is used to render the auto-index page."""
 
+    shared_head_html = ''
+    """HTML to be inserted in the <head> of every page template."""
+
+    shared_body_html = ''
+    """HTML to be inserted in the <body> of every page template."""
+
     def __init__(self, page: page, *, shared: bool = False) -> None:
         self.id = str(uuid.uuid4())
         self.created = time.time()
@@ -59,8 +63,8 @@ class Client:
 
         self.waiting_javascript_commands: Dict[str, Any] = {}
 
-        self.head_html = ''
-        self.body_html = ''
+        self._head_html = ''
+        self._body_html = ''
 
         self.page = page
 
@@ -84,6 +88,16 @@ class Client:
         """Return True if the client is connected, False otherwise."""
         return self.environ is not None
 
+    @property
+    def head_html(self) -> str:
+        """Return the HTML code to be inserted in the <head> of the page template."""
+        return self.shared_head_html + self._head_html
+
+    @property
+    def body_html(self) -> str:
+        """Return the HTML code to be inserted in the <body> of the page template."""
+        return self.shared_body_html + self._body_html
+
     def __enter__(self):
         self.content.__enter__()
         return self

+ 1 - 1
nicegui/elements/chat_message.py

@@ -8,7 +8,7 @@ from .html import Html
 class ChatMessage(Element):
 
     def __init__(self,
-                 text: Union[str, List[str]] = ..., *,
+                 text: Union[str, List[str]] = ..., *,  # type: ignore
                  name: Optional[str] = None,
                  label: Optional[str] = None,
                  stamp: Optional[str] = None,

+ 18 - 3
nicegui/elements/highchart.py

@@ -1,10 +1,25 @@
 from .. import optional_features
+from ..element import Element
+from ..logging import log
+from .markdown import Markdown
 
 try:
     from nicegui_highcharts import highchart
     optional_features.register('highcharts')
     __all__ = ['highchart']
 except ImportError:
-    class highchart:  # type: ignore
-        def __init__(self, *args, **kwargs) -> None:
-            raise NotImplementedError('Highcharts is not installed. Please run `pip install nicegui[highcharts]`.')
+    class highchart(Element):  # type: ignore
+        def __init__(self, *args, **kwargs) -> None:  # pylint: disable=unused-argument
+            """Highcharts chart
+
+            An element to create a chart using `Highcharts <https://www.highcharts.com/>`_.
+            Updates can be pushed to the chart by changing the `options` property.
+            After data has changed, call the `update` method to refresh the chart.
+
+            Due to Highcharts' restrictive license, this element is not part of the standard NiceGUI package.
+            It is maintained in a `separate repository <https://github.com/zauberzeug/nicegui-highcharts/>`_
+            and can be installed with `pip install nicegui[highcharts]`.
+            """
+            super().__init__()
+            Markdown('Highcharts is not installed. Please run `pip install nicegui[highcharts]`.')
+            log.warning('Highcharts is not installed. Please run "pip install nicegui[highcharts]".')

+ 11 - 6
nicegui/elements/image.py

@@ -4,15 +4,20 @@ import time
 from pathlib import Path
 from typing import Union
 
-from PIL.Image import Image as PIL_Image
-
+from .. import optional_features
 from .mixins.source_element import SourceElement
 
+try:
+    from PIL.Image import Image as PIL_Image
+    optional_features.register('pillow')
+except ImportError:
+    pass
+
 
 class Image(SourceElement, component='image.js'):
     PIL_CONVERT_FORMAT = 'PNG'
 
-    def __init__(self, source: Union[str, Path, PIL_Image] = '') -> None:
+    def __init__(self, source: Union[str, Path, 'PIL_Image'] = '') -> None:
         """Image
 
         Displays an image.
@@ -22,8 +27,8 @@ class Image(SourceElement, component='image.js'):
         """
         super().__init__(source=source)
 
-    def _set_props(self, source: Union[str, Path]) -> None:
-        if isinstance(source, PIL_Image):
+    def _set_props(self, source: Union[str, Path, 'PIL_Image']) -> None:
+        if optional_features.has('pillow') and isinstance(source, PIL_Image):
             source = pil_to_base64(source, self.PIL_CONVERT_FORMAT)
         super()._set_props(source)
 
@@ -33,7 +38,7 @@ class Image(SourceElement, component='image.js'):
         self.update()
 
 
-def pil_to_base64(pil_image: PIL_Image, image_format: str) -> str:
+def pil_to_base64(pil_image: 'PIL_Image', image_format: str) -> str:
     """Convert a PIL image to a base64 string which can be used as image source.
 
     :param pil_image: the PIL image

+ 10 - 5
nicegui/elements/interactive_image.py

@@ -4,20 +4,25 @@ import time
 from pathlib import Path
 from typing import Any, Callable, List, Optional, Union, cast
 
-from PIL.Image import Image as PIL_Image
-
+from .. import optional_features
 from ..events import GenericEventArguments, MouseEventArguments, handle_event
 from .image import pil_to_base64
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
 
+try:
+    from PIL.Image import Image as PIL_Image
+    optional_features.register('pillow')
+except ImportError:
+    pass
+
 
 class InteractiveImage(SourceElement, ContentElement, component='interactive_image.js'):
     CONTENT_PROP = 'content'
     PIL_CONVERT_FORMAT = 'PNG'
 
     def __init__(self,
-                 source: Union[str, Path] = '', *,
+                 source: Union[str, Path, 'PIL_Image'] = '', *,
                  content: str = '',
                  on_mouse: Optional[Callable[..., Any]] = None,
                  events: List[str] = ['click'],
@@ -61,8 +66,8 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
             handle_event(on_mouse, arguments)
         self.on('mouse', handle_mouse)
 
-    def _set_props(self, source: Union[str, Path]) -> None:
-        if isinstance(source, PIL_Image):
+    def _set_props(self, source: Union[str, Path, 'PIL_Image']) -> None:
+        if optional_features.has('pillow') and isinstance(source, PIL_Image):
             source = pil_to_base64(source, self.PIL_CONVERT_FORMAT)
         super()._set_props(source)
 

+ 31 - 6
nicegui/functions/html.py

@@ -1,11 +1,36 @@
 from .. import context
+from ..client import Client
 
 
-def add_body_html(code: str) -> None:
-    """Add HTML code to the body of the page."""
-    context.get_client().body_html += code + '\n'
+def add_head_html(code: str, *, shared: bool = False) -> None:
+    """Add HTML code to the head of the page.
 
+    Note that this function can only be called before the page is sent to the client.
 
-def add_head_html(code: str) -> None:
-    """Add HTML code to the head of the page."""
-    context.get_client().head_html += code + '\n'
+    :param code: HTML code to add
+    :param shared: if True, the code is added to all pages
+    """
+    if shared:
+        Client.shared_head_html += code + '\n'
+    else:
+        client = context.get_client()
+        if client.has_socket_connection:
+            raise RuntimeError('Cannot add head HTML after the page has been sent to the client.')
+        client._head_html += code + '\n'  # pylint: disable=protected-access
+
+
+def add_body_html(code: str, *, shared: bool = False) -> None:
+    """Add HTML code to the body of the page.
+
+    Note that this function can only be called before the page is sent to the client.
+
+    :param code: HTML code to add
+    :param shared: if True, the code is added to all pages
+    """
+    if shared:
+        Client.shared_body_html += code + '\n'
+    else:
+        client = context.get_client()
+        if client.has_socket_connection:
+            raise RuntimeError('Cannot add body HTML after the page has been sent to the client.')
+        client._body_html += code + '\n'  # pylint: disable=protected-access

+ 9 - 3
nicegui/functions/open.py

@@ -2,10 +2,11 @@ from typing import Any, Callable, Union
 
 from .. import context
 from ..client import Client
+from ..element import Element
 from ..logging import log
 
 
-def open(target: Union[Callable[..., Any], str], new_tab: bool = False) -> None:  # pylint: disable=redefined-builtin
+def open(target: Union[Callable[..., Any], str, Element], new_tab: bool = False) -> None:  # pylint: disable=redefined-builtin
     """Open
 
     Can be used to programmatically trigger redirects for a specific client.
@@ -18,10 +19,15 @@ def open(target: Union[Callable[..., Any], str], new_tab: bool = False) -> None:
     all clients (i.e. browsers) connected to the page will open the target URL unless a socket is specified.
     User events like button clicks provide such a socket.
 
-    :param target: page function or string that is a an absolute URL or relative path from base URL
+    :param target: page function, NiceGUI element on the same page or string that is a an absolute URL or relative path from base URL
     :param new_tab: whether to open the target in a new tab (might be blocked by the browser)
     """
-    path = target if isinstance(target, str) else Client.page_routes[target]
+    if isinstance(target, str):
+        path = target
+    elif isinstance(target, Element):
+        path = f'#c{target.id}'
+    elif callable(target):
+        path = Client.page_routes[target]
     client = context.get_client()
     if client.has_socket_connection:
         client.open(path, new_tab)

+ 2 - 2
nicegui/native/native_mode.py

@@ -21,7 +21,7 @@ try:
         # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
         warnings.filterwarnings('ignore', category=DeprecationWarning)
         import webview
-    optional_features.register('native')
+    optional_features.register('webview')
 except ModuleNotFoundError:
     pass
 
@@ -104,7 +104,7 @@ def activate(host: str, port: int, title: str, width: int, height: int, fullscre
             time.sleep(0.1)
         _thread.interrupt_main()
 
-    if not optional_features.has('native'):
+    if not optional_features.has('webview'):
         log.error('Native mode is not supported in this configuration.\n'
                   'Please run "pip install pywebview" to use it.')
         sys.exit(1)

+ 5 - 4
nicegui/nicegui.py

@@ -11,7 +11,7 @@ from fastapi.responses import FileResponse, Response
 from fastapi.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 
-from . import air, background_tasks, binding, core, favicon, helpers, json, outbox, run
+from . import air, background_tasks, binding, core, favicon, helpers, json, outbox, run, welcome
 from .app import App
 from .client import Client
 from .dependencies import js_components, libraries
@@ -26,7 +26,7 @@ from .version import __version__
 
 @asynccontextmanager
 async def _lifespan(_: App):
-    _startup()
+    await _startup()
     yield
     await _shutdown()
 
@@ -76,9 +76,8 @@ def _get_component(key: str) -> FileResponse:
     raise HTTPException(status_code=404, detail=f'component "{key}" not found')
 
 
-def _startup() -> None:
+async def _startup() -> None:
     """Handle the startup event."""
-    # NOTE ping interval and timeout need to be lower than the reconnect timeout, but can't be too low
     if not app.config.has_run_config:
         raise RuntimeError('\n\n'
                            'You must call ui.run() to start the server.\n'
@@ -87,6 +86,8 @@ def _startup() -> None:
                            'remove the guard or replace it with\n'
                            '   if __name__ in {"__main__", "__mp_main__"}:\n'
                            'to allow for multiprocessing.')
+    await welcome.collect_urls()
+    # NOTE ping interval and timeout need to be lower than the reconnect timeout, but can't be too low
     sio.eio.ping_interval = max(app.config.reconnect_timeout * 0.8, 4)
     sio.eio.ping_timeout = max(app.config.reconnect_timeout * 0.4, 2)
     if core.app.config.favicon:

+ 12 - 3
nicegui/optional_features.py

@@ -1,13 +1,22 @@
-from typing import Set
+from typing import Literal, Set
 
 _optional_features: Set[str] = set()
 
+FEATURE = Literal[
+    'highcharts',
+    'matplotlib',
+    'pandas',
+    'pillow',
+    'plotly',
+    'webview',
+]
 
-def register(feature: str) -> None:
+
+def register(feature: FEATURE) -> None:
     """Register an optional feature."""
     _optional_features.add(feature)
 
 
-def has(feature: str) -> bool:
+def has(feature: FEATURE) -> bool:
     """Check if an optional feature is registered."""
     return feature in _optional_features

+ 10 - 1
nicegui/page.py

@@ -12,6 +12,7 @@ from . import background_tasks, binding, core, helpers
 from .client import Client
 from .favicon import create_favicon_route
 from .language import Language
+from .logging import log
 
 if TYPE_CHECKING:
     from .api_router import APIRouter
@@ -87,6 +88,10 @@ class page:
         core.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
         parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
 
+        def check_for_late_return_value(task: asyncio.Task) -> None:
+            if task.result() is not None:
+                log.error(f'ignoring {task.result()}; it was returned after the HTML had been delivered to the client')
+
         async def decorated(*dec_args, **dec_kwargs) -> Response:
             request = dec_kwargs['request']
             # NOTE cleaning up the keyword args so the signature is consistent with "func" again
@@ -105,7 +110,11 @@ class page:
                     if time.time() > deadline:
                         raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
                     await asyncio.sleep(0.1)
-                result = task.result() if task.done() else None
+                if task.done():
+                    result = task.result()
+                else:
+                    result = None
+                    task.add_done_callback(check_for_late_return_value)
             if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
                 return result
             binding._refresh_step()  # pylint: disable=protected-access

+ 11 - 6
nicegui/ui_run.py

@@ -11,7 +11,6 @@ from uvicorn.supervisors import ChangeReload, Multiprocess
 
 from . import air, core, helpers
 from . import native as native_module
-from . import welcome
 from .client import Client
 from .language import Language
 from .logging import log
@@ -22,7 +21,7 @@ APP_IMPORT_STRING = 'nicegui:app'
 
 def run(*,
         host: Optional[str] = None,
-        port: int = 8080,
+        port: Optional[int] = None,
         title: str = 'NiceGUI',
         viewport: str = 'width=device-width, initial-scale=1',
         favicon: Optional[Union[str, Path]] = None,
@@ -45,6 +44,7 @@ def run(*,
         prod_js: bool = True,
         endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
         storage_secret: Optional[str] = None,
+        show_welcome_message: bool = True,
         **kwargs: Any,
         ) -> None:
     """ui.run
@@ -53,7 +53,7 @@ def run(*,
     Most of them only apply after stopping and fully restarting the app and do not apply with auto-reloading.
 
     :param host: start server with this host (defaults to `'127.0.0.1` in native mode, otherwise `'0.0.0.0'`)
-    :param port: use this port (default: `8080`)
+    :param port: use this port (default: 8080 in normal mode, and an automatically determined open port in native mode)
     :param title: page title (default: `'NiceGUI'`, can be overwritten per page)
     :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
     :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
@@ -76,6 +76,7 @@ def run(*,
     :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
     :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
     :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
+    :param show_welcome_message: whether to show the welcome message (default: `True`)
     :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
     """
     core.app.config.add_run_config(
@@ -89,6 +90,7 @@ def run(*,
         reconnect_timeout=reconnect_timeout,
         tailwind=tailwind,
         prod_js=prod_js,
+        show_welcome_message=show_welcome_message,
     )
     core.app.config.endpoint_documentation = endpoint_documentation
 
@@ -103,8 +105,6 @@ def run(*,
     if on_air:
         air.instance = air.Air('' if on_air is True else on_air)
 
-    core.app.on_startup(welcome.print_message)
-
     if multiprocessing.current_process().name != 'MainProcess':
         return
 
@@ -121,12 +121,14 @@ def run(*,
     if native:
         show = False
         host = host or '127.0.0.1'
-        port = native_module.find_open_port()
+        port = port or native_module.find_open_port()
         width, height = window_size or (800, 600)
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
     else:
+        port = port or 8080
         host = host or '0.0.0.0'
     assert host is not None
+    assert port is not None
 
     # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
     os.environ['NICEGUI_HOST'] = host
@@ -138,6 +140,9 @@ def run(*,
     def split_args(args: str) -> List[str]:
         return [a.strip() for a in args.split(',')]
 
+    if kwargs.get('workers', 1) > 1:
+        raise ValueError('NiceGUI does not support multiple workers yet.')
+
     # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
 
     config = CustomServerConfig(

+ 3 - 2
nicegui/ui_run_with.py

@@ -49,6 +49,7 @@ def run_with(
         reconnect_timeout=reconnect_timeout,
         tailwind=tailwind,
         prod_js=prod_js,
+        show_welcome_message=False,
     )
 
     storage.set_storage_secret(storage_secret)
@@ -58,9 +59,9 @@ def run_with(
 
     @asynccontextmanager
     async def lifespan_wrapper(app):
-        _startup()
+        await _startup()
         async with main_app_lifespan(app):
             yield
-        _shutdown()
+        await _shutdown()
 
     app.router.lifespan_context = lifespan_wrapper

+ 7 - 5
nicegui/welcome.py

@@ -13,15 +13,17 @@ def _get_all_ips() -> List[str]:
     return ips
 
 
-async def print_message() -> None:
+async def collect_urls() -> None:
     """Print a welcome message with URLs to access the NiceGUI app."""
-    print('NiceGUI ready to go ', end='', flush=True)
-    host = os.environ['NICEGUI_HOST']
-    port = os.environ['NICEGUI_PORT']
+    host = os.environ.get('NICEGUI_HOST')
+    port = os.environ.get('NICEGUI_PORT')
+    if not host or not port:
+        return
     ips = set((await run.io_bound(_get_all_ips)) if host == '0.0.0.0' else [])
     ips.discard('127.0.0.1')
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     core.app.urls.update(urls)
     if len(urls) >= 2:
         urls[-1] = 'and ' + urls[-1]
-    print(f'on {", ".join(urls)}', flush=True)
+    if core.app.config.show_welcome_message:
+        print(f'NiceGUI ready to go on {", ".join(urls)}', flush=True)

+ 9 - 4
tests/screen.py

@@ -1,8 +1,9 @@
 import os
+import re
 import threading
 import time
 from contextlib import contextmanager
-from typing import List, Optional
+from typing import List, Optional, Union
 
 import pytest
 from selenium import webdriver
@@ -194,14 +195,18 @@ class Screen:
         print(f'Storing screenshot to {filename}')
         self.selenium.get_screenshot_as_file(filename)
 
-    def assert_py_logger(self, level: str, message: str) -> None:
-        """Assert that the Python logger has received a message with the given level and text."""
+    def assert_py_logger(self, level: str, message: Union[str, re.Pattern]) -> None:
+        """Assert that the Python logger has received a message with the given level and text or regex pattern."""
         try:
             assert self.caplog.records, 'Expected a log message'
             record = self.caplog.records[0]
             print(record.levelname, record.message)
             assert record.levelname.strip() == level, f'Expected "{level}" but got "{record.levelname}"'
-            assert record.message.strip() == message, f'Expected "{message}" but got "{record.message}"'
+
+            if isinstance(message, re.Pattern):
+                assert message.search(record.message), f'Expected regex "{message}" but got "{record.message}"'
+            else:
+                assert record.message.strip() == message, f'Expected "{message}" but got "{record.message}"'
         finally:
             self.caplog.records.clear()
 

+ 13 - 0
tests/test_page.py

@@ -1,4 +1,5 @@
 import asyncio
+import re
 from uuid import uuid4
 
 from fastapi.responses import PlainTextResponse
@@ -286,6 +287,18 @@ def test_returning_custom_response_async(screen: Screen):
     screen.should_not_contain('normal NiceGUI page')
 
 
+def test_warning_about_to_late_responses(screen: Screen):
+    @ui.page('/')
+    async def page(client: Client):
+        await client.connected()
+        ui.label('NiceGUI page')
+        return PlainTextResponse('custom response')
+
+    screen.open('/')
+    screen.should_contain('NiceGUI page')
+    screen.assert_py_logger('ERROR', re.compile('it was returned after the HTML had been delivered to the client'))
+
+
 def test_reconnecting_without_page_reload(screen: Screen):
     @ui.page('/', reconnect_timeout=3.0)
     def page():

+ 4 - 15
website/__init__.py

@@ -1,20 +1,9 @@
-from . import documentation, example_card, svg
-from .search import Search
-from .star import add_star
-from .style import example_link, features, heading, link_target, section_heading, side_menu, subtitle, title
+from . import anti_scroll_hack, documentation, fly, main_page, svg
 
 __all__ = [
+    'anti_scroll_hack',
     'documentation',
-    'example_card',
+    'fly',
+    'main_page',
     'svg',
-    'Search',
-    'add_star',
-    'example_link',
-    'features',
-    'heading',
-    'link_target',
-    'section_heading',
-    'side_menu',
-    'subtitle',
-    'title',
 ]

+ 13 - 0
website/anti_scroll_hack.py

@@ -0,0 +1,13 @@
+from nicegui import ui
+
+
+def setup() -> None:
+    """Prevent the page from scrolling when closing a dialog."""
+    # HACK (issue #1404)
+    # pylint: disable=protected-access
+    def _handle_value_change(sender, value, on_value_change=ui.dialog._handle_value_change) -> None:
+        ui.query('html').classes(**{'add' if value else 'remove': 'has-dialog'})
+        on_value_change(sender, value)
+
+    # pylint: disable=method-assign
+    ui.dialog._handle_value_change = _handle_value_change  # type: ignore

+ 0 - 160
website/build_search_index.py

@@ -1,160 +0,0 @@
-#!/usr/bin/env python3
-import ast
-import inspect
-import json
-import os
-import re
-from _ast import AsyncFunctionDef
-from pathlib import Path
-from typing import List, Optional, Union
-
-from nicegui import app, ui
-
-dir_path = Path(__file__).parent
-os.chdir(dir_path)
-
-
-def ast_string_node_to_string(node):
-    if isinstance(node, ast.Str):
-        return node.s
-    elif isinstance(node, ast.JoinedStr):
-        return ''.join(ast_string_node_to_string(part) for part in node.values)
-    else:
-        return str(ast.unparse(node))
-
-
-def cleanup(markdown_string: str) -> str:
-    # Remove link URLs but keep the description
-    markdown_string = re.sub(r'\[([^\[]+)\]\([^\)]+\)', r'\1', markdown_string)
-    # Remove inline code ticks
-    markdown_string = re.sub(r'`([^`]+)`', r'\1', markdown_string)
-    # Remove code blocks
-    markdown_string = re.sub(r'```([^`]+)```', r'\1', markdown_string)
-    markdown_string = re.sub(r'``([^`]+)``', r'\1', markdown_string)
-    # Remove braces
-    markdown_string = re.sub(r'\{([^\}]+)\}', r'\1', markdown_string)
-    return markdown_string
-
-
-class DocVisitor(ast.NodeVisitor):
-
-    def __init__(self, topic: Optional[str] = None) -> None:
-        super().__init__()
-        self.topic = topic
-        self.current_title = None
-        self.current_content: List[str] = []
-
-    def visit_Call(self, node: ast.Call):
-        if isinstance(node.func, ast.Name):
-            function_name = node.func.id
-        elif isinstance(node.func, ast.Attribute):
-            function_name = node.func.attr
-        else:
-            raise NotImplementedError(f'Unknown function type: {node.func}')
-        if function_name in ['heading', 'subheading']:
-            self._handle_new_heading()
-            self.current_title = node.args[0].s
-        elif function_name == 'markdown':
-            if node.args:
-                raw = ast_string_node_to_string(node.args[0]).splitlines()
-                raw = ' '.join(l.strip() for l in raw).strip()
-                self.current_content.append(cleanup(raw))
-        self.generic_visit(node)
-
-    def _handle_new_heading(self) -> None:
-        if self.current_title:
-            self.add_to_search_index(self.current_title, self.current_content if self.current_content else 'Overview')
-            self.current_content = []
-
-    def visit_AsyncFunctionDef(self, node: AsyncFunctionDef) -> None:
-        self.visit_FunctionDef(node)
-
-    def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
-        if node.name == 'main_demo':
-            docstring = ast.get_docstring(node)
-            if docstring is None:
-                api = getattr(ui, self.topic) if hasattr(ui, self.topic) else getattr(app, self.topic)
-                docstring = api.__doc__ or api.__init__.__doc__
-                for name, method in api.__dict__.items():
-                    if not name.startswith('_') and inspect.isfunction(method):
-                        # add method name to docstring
-                        docstring += name + ' '
-                        docstring += method.__doc__ or ''
-            lines = cleanup(docstring).splitlines()
-            self.add_to_search_index(lines[0], lines[1:], main=True)
-
-        for decorator in node.decorator_list:
-            if isinstance(decorator, ast.Call):
-                function = decorator.func
-                if isinstance(function, ast.Name) and function.id == 'text_demo':
-                    title = decorator.args[0].s
-                    content = cleanup(decorator.args[1].s).splitlines()
-                    self.add_to_search_index(title, content)
-                if isinstance(function, ast.Name) and function.id == 'element_demo':
-                    attr_name = decorator.args[0].attr
-                    obj_name = decorator.args[0].value.id
-                    if obj_name == 'app':
-                        docstring: str = getattr(app, attr_name).__doc__
-                        docstring = ' '.join(l.strip() for l in docstring.splitlines()).strip()
-                        self.current_content.append(cleanup(docstring))
-                    else:
-                        print(f'Unknown object: {obj_name} for element_demo', flush=True)
-        self.generic_visit(node)
-
-    def add_to_search_index(self, title: str, content: Union[str, list], main: bool = False) -> None:
-        if isinstance(content, list):
-            content_str = ' '.join(l.strip() for l in content).strip()
-        else:
-            content_str = content
-
-        anchor = title.lower().replace(' ', '_')
-        url = f'/documentation/{self.topic or ""}'
-        if not main:
-            url += f'#{anchor}'
-            if self.topic:
-                title = f'{self.topic.replace("_", " ").title()}: {title}'
-        documents.append({
-            'title': title,
-            'content': content_str,
-            'url': url,
-        })
-
-
-class MainVisitor(ast.NodeVisitor):
-
-    def visit_Call(self, node: ast.Call):
-        if isinstance(node.func, ast.Name):
-            function_name = node.func.id
-        elif isinstance(node.func, ast.Attribute):
-            function_name = node.func.attr
-        else:
-            return
-        if function_name == 'example_link':
-            title = ast_string_node_to_string(node.args[0])
-            name = title.lower().replace(' ', '_')
-            path = Path(__file__).parent.parent / 'examples' / name
-            file = 'main.py' if (path / 'main.py').is_file() else ''
-            documents.append({
-                'title': 'Example: ' + title,
-                'content': ast_string_node_to_string(node.args[1]),
-                'url': f'https://github.com/zauberzeug/nicegui/tree/main/examples/{name}/{file}',
-            })
-
-
-def generate_for(file: Path, topic: Optional[str] = None) -> None:
-    tree = ast.parse(file.read_text())
-    doc_visitor = DocVisitor(topic)
-    doc_visitor.visit(tree)
-    if doc_visitor.current_title:
-        doc_visitor._handle_new_heading()  # to finalize the last heading
-
-
-documents = []
-tree = ast.parse(Path('../main.py').read_text())
-MainVisitor().visit(tree)
-
-for file in Path('./documentation/more').glob('*.py'):
-    generate_for(file, file.stem.removesuffix('_documentation'))
-
-with open('static/search_index.json', 'w') as f:
-    json.dump(documents, f, indent=2)

+ 8 - 10
website/documentation/__init__.py

@@ -1,18 +1,16 @@
-from . import more
-from .content import create_overview, create_section
-from .demo import bash_window, browser_window, python_window
+from .content import overview, registry
 from .intro import create_intro
-from .tools import create_anchor_name, element_demo, generate_class_doc
+from .rendering import render_page
+from .search import build_search_index
+from .windows import bash_window, browser_window, python_window
 
 __all__ = [
     'bash_window',
     'browser_window',
-    'create_anchor_name',
-    'create_overview',
-    'create_section',
-    'more',
+    'build_search_index',
     'create_intro',
-    'element_demo',
-    'generate_class_doc',
+    'overview',  # ensure documentation tree is built
     'python_window',
+    'registry',
+    'render_page',
 ]

+ 0 - 110
website/documentation/content.py

@@ -1,110 +0,0 @@
-from typing import Dict
-
-from nicegui import ui
-
-from .section import Section
-from .sections import (action_events, audiovisual_elements, binding_properties, configuration_deployment, controls,
-                       data_elements, page_layout, pages_routing, styling_appearance, text_elements)
-from .tools import heading
-
-SECTIONS: Dict[str, Section] = {
-    section.name: section
-    for section in [
-        text_elements,
-        controls,
-        audiovisual_elements,
-        data_elements,
-        binding_properties,
-        page_layout,
-        styling_appearance,
-        action_events,
-        pages_routing,
-        configuration_deployment,
-    ]
-}
-
-
-def create_overview() -> None:
-    ui.markdown('''
-        ### Overview
-                                
-        NiceGUI is an open-source Python library to write graphical user interfaces which run in the browser.
-        It has a very gentle learning curve while still offering the option for advanced customizations.
-        NiceGUI follows a backend-first philosophy:
-        It handles all the web development details.
-        You can focus on writing Python code. 
-        This makes it ideal for a wide range of projects including short 
-        scripts, dashboards, robotics projects, IoT solutions, smart home automation, and machine learning.
-
-        ### How to use this guide
-
-        This documentation explains how to use NiceGUI.
-        Each of the tiles covers a NiceGUI topic in detail.
-        It is recommended to start by reading this entire introduction page, then refer to other sections as needed.
-
-        ### Basic concepts
-
-        NiceGUI provides UI _components_ (or _elements_) such as buttons, sliders, text, images, charts, and more.
-        Your app assembles these components into _pages_.
-        When the user interacts with an item on a page, NiceGUI triggers an _event_ (or _action_).
-        You define code to _handle_ each event, such as what to do when a user clicks a button named "Go".
-
-        Components are arranged on a page using _layouts_.
-        Layouts provide things like grids, tabs, carousels, expansions, menus, and other tools to arrange your components.
-        Many components are linked to a _model_ (data object) in your code, which automatically updates the user interface when the value changes.
-
-        Styling and appearance can be controlled in several ways.
-        NiceGUI accepts optional arguments for certain styling, such as icons on buttons.
-        Other styling can be set with functions such as `.styles`, `.classes`, or `.props` that you'll learn about later.
-        Global styles like colors and fonts can be set with dedicated properties.
-        Or if you prefer, almost anything can be styled with CSS.
-    ''')
-    with ui.grid().classes('grid-cols-[1fr] md:grid-cols-[1fr_1fr] xl:grid-cols-[1fr_1fr_1fr]'):
-        for section in SECTIONS.values():
-            with ui.link(target=f'/documentation/section_{section.name}/') \
-                    .classes('bg-[#5898d420] p-4 self-stretch rounded flex flex-col gap-2') \
-                    .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
-                ui.label(section.title).classes(replace='text-2xl')
-                ui.markdown(section.description).classes(replace='bold-links arrow-links')
-
-    ui.markdown('''
-        ### Actions
-
-        NiceGUI runs an event loop to handle user input and other events like timers and keyboard bindings.
-        You can write asynchronous functions for long-running tasks to keep the UI responsive.
-        The _Actions_ section covers how to work with events.
-
-        ### Implementation
-        
-        NiceGUI is implemented with HTML components served by an HTTP server (FastAPI), even for native windows.
-        If you already know HTML, everything will feel very familiar.
-        If you don't know HTML, that's fine too!
-        NiceGUI abstracts away the details, so you can focus on creating beautiful interfaces without worrying about how they are implemented.
-
-        ### Running NiceGUI Apps
-
-        There are several options for deploying NiceGUI.
-        By default, NiceGUI runs a server on localhost and runs your app as a private web page on the local machine.
-        When run this way, your app appears in a web browser window.
-        You can also run NiceGUI in a native window separate from a web browser.
-        Or you can run NiceGUI on a server that handles many clients - the website you're reading right now is served from NiceGUI.
-
-        After creating your app pages with components, you call `ui.run()` to start the NiceGUI server.
-        Optional parameters to `ui.run` set things like the network address and port the server binds to, 
-        whether the app runs in native mode, initial window size, and many other options.
-        The section _Configuration and Deployment_ covers the options to the `ui.run()` function and the FastAPI framework it is based on.
-
-        ### Customization
-
-        If you want more customization in your app, you can use the underlying Tailwind classes and Quasar components
-        to control the style or behavior of your components.
-        You can also extend the available components by subclassing existing NiceGUI components or importing new ones from Quasar.
-        All of this is optional.
-        Out of the box, NiceGUI provides everything you need to make modern, stylish, responsive user interfaces.
-    ''')
-
-
-def create_section(name: str) -> None:
-    section = SECTIONS[name]
-    heading(section.title)
-    section.content()

+ 7 - 0
website/documentation/content/__init__.py

@@ -0,0 +1,7 @@
+from .doc import registry
+from .doc.page import DocumentationPage
+
+__all__ = [
+    'DocumentationPage',
+    'registry',
+]

+ 4 - 1
website/documentation/more/add_static_files_documentation.py → website/documentation/content/add_static_files_documentation.py

@@ -1,6 +1,9 @@
-from nicegui import ui
+from nicegui import app, ui
 
+from . import doc
 
+
+@doc.demo(app.add_static_files)
 def main_demo() -> None:
     from nicegui import app
 

+ 198 - 0
website/documentation/content/aggrid_documentation.py

@@ -0,0 +1,198 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.aggrid)
+def main_demo() -> None:
+    grid = ui.aggrid({
+        'defaultColDef': {'flex': 1},
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name'},
+            {'headerName': 'Age', 'field': 'age'},
+            {'headerName': 'Parent', 'field': 'parent', 'hide': True},
+        ],
+        'rowData': [
+            {'name': 'Alice', 'age': 18, 'parent': 'David'},
+            {'name': 'Bob', 'age': 21, 'parent': 'Eve'},
+            {'name': 'Carol', 'age': 42, 'parent': 'Frank'},
+        ],
+        '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'))
+    ui.button('Show parent', on_click=lambda: grid.call_column_api_method('setColumnVisible', 'parent', True))
+
+
+@doc.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)
+
+
+@doc.demo('Filter Rows using Mini Filters', '''
+    You can add [mini filters](https://ag-grid.com/javascript-data-grid/filter-set-mini-filter/)
+    to the header of each column to filter the rows.
+    
+    Note how the "agTextColumnFilter" matches individual characters, like "a" in "Alice" and "Carol",
+    while the "agNumberColumnFilter" matches the entire number, like "18" and "21", but not "1".
+''')
+def aggrid_with_minifilters():
+    ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name', 'filter': 'agTextColumnFilter', 'floatingFilter': True},
+            {'headerName': 'Age', 'field': 'age', 'filter': 'agNumberColumnFilter', 'floatingFilter': True},
+        ],
+        'rowData': [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol', 'age': 42},
+        ],
+    }).classes('max-h-40')
+
+
+@doc.demo('AG Grid with Conditional Cell Formatting', '''
+    This demo shows how to use [cellClassRules](https://www.ag-grid.com/javascript-grid-cell-styles/#cell-class-rules)
+    to conditionally format cells based on their values.
+''')
+def aggrid_with_conditional_cell_formatting():
+    ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name'},
+            {'headerName': 'Age', 'field': 'age', 'cellClassRules': {
+                'bg-red-300': 'x < 21',
+                'bg-green-300': 'x >= 21',
+            }},
+        ],
+        'rowData': [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol', 'age': 42},
+        ],
+    })
+
+
+@doc.demo('Create Grid from Pandas DataFrame', '''
+    You can create an AG Grid from a Pandas DataFrame using the `from_pandas` method.
+    This method takes a Pandas DataFrame as input and returns an AG Grid.
+''')
+def aggrid_from_pandas():
+    import pandas as pd
+
+    df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]})
+    ui.aggrid.from_pandas(df).classes('max-h-40')
+
+
+@doc.demo('Render columns as HTML', '''
+    You can render columns as HTML by passing a list of column indices to the `html_columns` argument.
+''')
+def aggrid_with_html_columns():
+    ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name'},
+            {'headerName': 'URL', 'field': 'url'},
+        ],
+        'rowData': [
+            {'name': 'Google', 'url': '<a href="https://google.com">https://google.com</a>'},
+            {'name': 'Facebook', 'url': '<a href="https://facebook.com">https://facebook.com</a>'},
+        ],
+    }, html_columns=[1])
+
+
+@doc.demo('Respond to an AG Grid event', '''
+    All AG Grid events are passed through to NiceGUI via the AG Grid global listener.
+    These events can be subscribed to using the `.on()` method.
+''')
+def aggrid_respond_to_event():
+    ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'Name', 'field': 'name'},
+            {'headerName': 'Age', 'field': 'age'},
+        ],
+        'rowData': [
+            {'name': 'Alice', 'age': 18},
+            {'name': 'Bob', 'age': 21},
+            {'name': 'Carol', 'age': 42},
+        ],
+    }).on('cellClicked', lambda event: ui.notify(f'Cell value: {event.args["value"]}'))
+
+
+@doc.demo('AG Grid with complex objects', '''
+    You can use nested complex objects in AG Grid by separating the field names with a period.
+    (This is the reason why keys in `rowData` are not allowed to contain periods.)
+''')
+def aggrid_with_complex_objects():
+    ui.aggrid({
+        'columnDefs': [
+            {'headerName': 'First name', 'field': 'name.first'},
+            {'headerName': 'Last name', 'field': 'name.last'},
+            {'headerName': 'Age', 'field': 'age'}
+        ],
+        'rowData': [
+            {'name': {'first': 'Alice', 'last': 'Adams'}, 'age': 18},
+            {'name': {'first': 'Bob', 'last': 'Brown'}, 'age': 21},
+            {'name': {'first': 'Carol', 'last': 'Clark'}, 'age': 42},
+        ],
+    }).classes('max-h-40')
+
+
+@doc.demo('AG Grid with dynamic row height', '''
+    You can set the height of individual rows by passing a function to the `getRowHeight` argument.
+''')
+def aggrid_with_dynamic_row_height():
+    ui.aggrid({
+        'columnDefs': [{'field': 'name'}, {'field': 'age'}],
+        'rowData': [
+            {'name': 'Alice', 'age': '18'},
+            {'name': 'Bob', 'age': '21'},
+            {'name': 'Carol', 'age': '42'},
+        ],
+        ':getRowHeight': 'params => params.data.age > 35 ? 50 : 25',
+    }).classes('max-h-40')
+
+
+doc.reference(ui.aggrid)

+ 25 - 0
website/documentation/content/audio_documentation.py

@@ -0,0 +1,25 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.audio)
+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'), icon='volume_off').props('outline')
+    ui.button(on_click=lambda: a.props(remove='muted'), icon='volume_up').props('outline')
+
+
+@doc.demo('Control the audio element', '''
+    This demo shows how to play, pause and seek programmatically.
+''')
+def control_demo() -> None:
+    a = ui.audio('https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3')
+    ui.button('Play', on_click=a.play)
+    ui.button('Pause', on_click=a.pause)
+    ui.button('Jump to 0:30', on_click=lambda: a.seek(30))
+
+
+doc.reference(ui.audio)

+ 20 - 0
website/documentation/content/avatar_documentation.py

@@ -0,0 +1,20 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.avatar)
+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')
+
+
+@doc.demo('Photos', '''
+    To use a photo as an avatar, you can use `ui.image` within `ui.avatar`.
+''')
+def photos() -> None:
+    with ui.avatar():
+        ui.image('https://robohash.org/robot?bgset=bg2')
+
+
+doc.reference(ui.avatar)

+ 6 - 0
website/documentation/more/badge_documentation.py → website/documentation/content/badge_documentation.py

@@ -1,6 +1,12 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.badge)
 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')
+
+
+doc.reference(ui.badge)

+ 64 - 0
website/documentation/content/button_documentation.py

@@ -0,0 +1,64 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.button)
+def main_demo() -> None:
+    ui.button('Click me!', on_click=lambda: ui.notify('You clicked me!'))
+
+
+@doc.demo('Icons', '''
+    You can also add an icon to a button.
+''')
+def icons() -> None:
+    with ui.row():
+        ui.button('demo', icon='history')
+        ui.button(icon='thumb_up')
+        with ui.button():
+            ui.label('sub-elements')
+            ui.image('https://picsum.photos/id/377/640/360') \
+                .classes('rounded-full w-16 h-16 ml-4')
+
+
+@doc.demo('Await button click', '''
+    Sometimes it is convenient to wait for a button click before continuing the execution.
+''')
+async def await_button_click() -> None:
+    # @ui.page('/')
+    # async def index():
+    b = ui.button('Step')
+    await b.clicked()
+    ui.label('One')
+    await b.clicked()
+    ui.label('Two')
+    await b.clicked()
+    ui.label('Three')
+
+
+@doc.demo('Disable button with a context manager', '''
+    This showcases a context manager that can be used to disable a button for the duration of an async process.
+''')
+def disable_context_manager() -> None:
+    from contextlib import contextmanager
+
+    import httpx
+
+    @contextmanager
+    def disable(button: ui.button):
+        button.disable()
+        try:
+            yield
+        finally:
+            button.enable()
+
+    async def get_slow_response(button: ui.button) -> None:
+        with disable(button):
+            async with httpx.AsyncClient() as client:
+                response = await client.get('https://httpbin.org/delay/1', timeout=5)
+                ui.notify(f'Response code: {response.status_code}')
+
+    ui.button('Get slow response', on_click=lambda e: get_slow_response(e.sender))
+
+
+doc.reference(ui.button)

+ 53 - 0
website/documentation/content/card_documentation.py

@@ -0,0 +1,53 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.card)
+def main_demo() -> None:
+    with ui.card().tight():
+        ui.image('https://picsum.photos/id/684/640/360')
+        with ui.card_section():
+            ui.label('Lorem ipsum dolor sit amet, consectetur adipiscing elit, ...')
+
+
+@doc.demo('Card without shadow', '''
+    You can remove the shadow from a card by adding the `no-shadow` class.
+    The following demo shows a 1 pixel wide border instead.
+''')
+def card_without_shadow() -> None:
+    with ui.card().classes('no-shadow border-[1px]'):
+        ui.label('See, no shadow!')
+
+
+@doc.demo('The issue with nested borders', '''
+    The following example shows a table nested in a card.
+    Cards have a default padding in NiceGUI, so the table is not flush with the card's border.
+    The table has the `flat` and `bordered` props set, so it should have a border.
+    However, due to the way QCard is designed, the border is not visible (first card).
+    There are two ways to fix this:
+
+    - To get the original QCard behavior, use the `tight` method (second card).
+        It removes the padding and the table border collapses with the card border.
+    
+    - To preserve the padding _and_ the table border, move the table into another container like a `ui.row` (third card).
+
+    See https://github.com/zauberzeug/nicegui/issues/726 for more information.
+''')
+def custom_context_menu() -> None:
+    columns = [{'name': 'age', 'label': 'Age', 'field': 'age'}]
+    rows = [{'age': '16'}, {'age': '18'}, {'age': '21'}]
+
+    with ui.row():
+        with ui.card():
+            ui.table(columns, rows).props('flat bordered')
+
+        with ui.card().tight():
+            ui.table(columns, rows).props('flat bordered')
+
+        with ui.card():
+            with ui.row():
+                ui.table(columns, rows).props('flat bordered')
+
+
+doc.reference(ui.card)

+ 6 - 0
website/documentation/more/carousel_documentation.py → website/documentation/content/carousel_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.carousel)
 def main_demo() -> None:
     with ui.carousel(animated=True, arrows=True, navigation=True).props('height=180px'):
         with ui.carousel_slide().classes('p-0'):
@@ -9,3 +12,6 @@ def main_demo() -> None:
             ui.image('https://picsum.photos/id/31/270/180').classes('w-[270px]')
         with ui.carousel_slide().classes('p-0'):
             ui.image('https://picsum.photos/id/32/270/180').classes('w-[270px]')
+
+
+doc.reference(ui.carousel)

+ 46 - 0
website/documentation/content/chat_message_documentation.py

@@ -0,0 +1,46 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.chat_message)
+def main_demo() -> None:
+    ui.chat_message('Hello NiceGUI!',
+                    name='Robot',
+                    stamp='now',
+                    avatar='https://robohash.org/ui')
+
+
+@doc.demo('HTML text', '''
+    Using the `text_html` parameter, you can send HTML text to the chat.
+''')
+def html_text():
+    ui.chat_message('Without <strong>HTML</strong>')
+    ui.chat_message('With <strong>HTML</strong>', text_html=True)
+
+
+@doc.demo('Newline', '''
+    You can use newlines in the chat message.
+''')
+def newline():
+    ui.chat_message('This is a\nlong line!')
+
+
+@doc.demo('Multi-part messages', '''
+    You can send multiple message parts by passing a list of strings.
+''')
+def multiple_messages():
+    ui.chat_message(['Hi! 😀', 'How are you?']
+                    )
+
+
+@doc.demo('Chat message with child elements', '''
+    You can add child elements to a chat message.
+''')
+def child_elements():
+    with ui.chat_message():
+        ui.label('Guess where I am!')
+        ui.image('https://picsum.photos/id/249/640/360').classes('w-64')
+
+
+doc.reference(ui.chat_message)

+ 6 - 0
website/documentation/more/checkbox_documentation.py → website/documentation/content/checkbox_documentation.py

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

+ 26 - 0
website/documentation/content/circular_progress_documentation.py

@@ -0,0 +1,26 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.circular_progress)
+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')
+
+
+@doc.demo('Nested Elements', '''
+    You can put any element like icon, button etc inside a circular progress using the `with` statement.
+    Just make sure it fits the bounds and disable the default behavior of showing the value.
+''')
+def icon() -> None:
+    with ui.row().classes('items-center m-auto'):
+        with ui.circular_progress(value=0.1, show_value=False) as progress:
+            ui.button(
+                icon='star',
+                on_click=lambda: progress.set_value(progress.value + 0.1)
+            ).props('flat round')
+        ui.label('click to increase progress')
+
+
+doc.reference(ui.circular_progress)

+ 6 - 0
website/documentation/more/code_documentation.py → website/documentation/content/code_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.code)
 def main_demo() -> None:
     ui.code('''
         from nicegui import ui
@@ -9,3 +12,6 @@ def main_demo() -> None:
             
         ui.run()
     ''').classes('w-full')
+
+
+doc.reference(ui.code)

+ 6 - 0
website/documentation/more/color_input_documentation.py → website/documentation/content/color_input_documentation.py

@@ -1,7 +1,13 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.color_input)
 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}'))
+
+
+doc.reference(ui.color_input)

+ 6 - 0
website/documentation/more/color_picker_documentation.py → website/documentation/content/color_picker_documentation.py

@@ -1,6 +1,12 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.color_picker)
 def main_demo() -> None:
     with ui.button(icon='colorize') as button:
         ui.color_picker(on_pick=lambda e: button.style(f'background-color:{e.color}!important'))
+
+
+doc.reference(ui.color_picker)

+ 8 - 2
website/documentation/more/colors_documentation.py → website/documentation/content/colors_documentation.py

@@ -1,9 +1,15 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.colors)
 def main_demo() -> None:
     # ui.button('Default', on_click=lambda: ui.colors())
     # ui.button('Gray', on_click=lambda: ui.colors(primary='#555'))
     # END OF DEMO
-    b1 = ui.button('Default', on_click=lambda: [b.classes(replace='!bg-primary') for b in {b1, b2}])
-    b2 = ui.button('Gray', on_click=lambda: [b.classes(replace='!bg-[#555]') for b in {b1, b2}])
+    b1 = ui.button('Default', on_click=lambda: [b.classes(replace='!bg-primary') for b in [b1, b2]])
+    b2 = ui.button('Gray', on_click=lambda: [b.classes(replace='!bg-[#555]') for b in [b1, b2]])
+
+
+doc.reference(ui.colors)

+ 26 - 0
website/documentation/content/column_documentation.py

@@ -0,0 +1,26 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.column)
+def main_demo() -> None:
+    with ui.column():
+        ui.label('label 1')
+        ui.label('label 2')
+        ui.label('label 3')
+
+
+@doc.demo('Masonry or Pinterest-Style Layout', '''
+    To create a masonry/Pinterest layout, the normal `ui.column` can not be used.
+    But it can be achieved with a few TailwindCSS classes.
+''')
+def masonry() -> None:
+    with ui.element('div').classes('columns-3 w-full gap-2'):
+        for i, height in enumerate([50, 50, 50, 150, 100, 50]):
+            tailwind = f'mb-2 p-2 h-[{height}px] bg-blue-100 break-inside-avoid'
+            with ui.card().classes(tailwind):
+                ui.label(f'Card #{i+1}')
+
+
+doc.reference(ui.column)

+ 6 - 0
website/documentation/more/context_menu_documentation.py → website/documentation/content/context_menu_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.context_menu)
 def main_demo() -> None:
     with ui.image('https://picsum.photos/id/377/640/360'):
         with ui.context_menu():
@@ -8,3 +11,6 @@ def main_demo() -> None:
             ui.menu_item('Flip vertically')
             ui.separator()
             ui.menu_item('Reset')
+
+
+doc.reference(ui.context_menu)

+ 6 - 1
website/documentation/more/dark_mode_documentation.py → website/documentation/content/dark_mode_documentation.py

@@ -1,8 +1,10 @@
 from nicegui import ui
 
-from ..demo import WINDOW_BG_COLORS
+from ..windows import WINDOW_BG_COLORS
+from . import doc
 
 
+@doc.demo(ui.dark_mode)
 def main_demo() -> None:
     # dark = ui.dark_mode()
     # ui.label('Switch mode:')
@@ -19,3 +21,6 @@ def main_demo() -> None:
         l.style('color: black'),
         c.style(f'background-color: {WINDOW_BG_COLORS["browser"][0]}'),
     ))
+
+
+doc.reference(ui.dark_mode)

+ 37 - 0
website/documentation/content/date_documentation.py

@@ -0,0 +1,37 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.date)
+def main_demo() -> None:
+    ui.date(value='2023-01-01', on_change=lambda e: result.set_text(e.value))
+    result = ui.label()
+
+
+@doc.demo('Input element with date picker', '''
+    This demo shows how to implement a date picker with an input element.
+    We place an icon in the input element's append slot.
+    When the icon is clicked, we open a menu with a date picker.
+
+    The date is bound to the input element's value.
+    So both the input element and the date picker will stay in sync whenever the date is changed.
+''')
+def date():
+    with ui.input('Date') as date:
+        with date.add_slot('append'):
+            ui.icon('edit_calendar').on('click', lambda: menu.open()).classes('cursor-pointer')
+        with ui.menu() as menu:
+            ui.date().bind_value(date)
+
+
+@doc.demo('Date filter', '''
+    This demo shows how to filter the dates in a date picker.
+    In order to pass a function to the date picker, we use the `:options` property.
+    The leading `:` tells NiceGUI that the value is a JavaScript expression.
+''')
+def date_filter():
+    ui.date().props('''default-year-month=2023/01 :options="date => date <= '2023/01/15'"''')
+
+
+doc.reference(ui.date)

+ 51 - 0
website/documentation/content/dialog_documentation.py

@@ -0,0 +1,51 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.dialog)
+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)
+
+
+@doc.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)
+
+
+@doc.demo('Replacing content', '''
+    The content of a dialog can be changed.
+''')
+def replace_content():
+    def replace():
+        dialog.clear()
+        with dialog, ui.card().classes('w-64 h-64'):
+            ui.label('New Content')
+        dialog.open()
+
+    with ui.dialog() as dialog, ui.card():
+        ui.label('Hello world!')
+
+    ui.button('Open', on_click=dialog.open)
+    ui.button('Replace', on_click=replace)
+
+
+doc.reference(ui.dialog)

+ 13 - 0
website/documentation/content/doc/__init__.py

@@ -0,0 +1,13 @@
+from .api import demo, extra_column, get_page, intro, reference, registry, text, title, ui
+
+__all__ = [
+    'demo',
+    'intro',
+    'reference',
+    'registry',
+    'text',
+    'title',
+    'ui',
+    'get_page',
+    'extra_column',
+]

+ 150 - 0
website/documentation/content/doc/api.py

@@ -0,0 +1,150 @@
+from __future__ import annotations
+
+import inspect
+import sys
+import types
+from copy import deepcopy
+from pathlib import Path
+from types import ModuleType
+from typing import Any, Callable, Dict, Optional, Union, overload
+
+from nicegui import app as nicegui_app
+from nicegui import ui as nicegui_ui
+from nicegui.elements.markdown import remove_indentation
+
+from .page import DocumentationPage
+from .part import Demo, DocumentationPart
+
+registry: Dict[str, DocumentationPage] = {}
+
+
+def get_page(documentation: ModuleType) -> DocumentationPage:
+    """Return the documentation page for the given documentation module."""
+    target_name = _removesuffix(documentation.__name__.split('.')[-1], '_documentation')
+    assert target_name in registry, f'Documentation page {target_name} does not exist'
+    return registry[target_name]
+
+
+def _get_current_page() -> DocumentationPage:
+    frame = sys._getframe(2)  # pylint: disable=protected-access
+    module = inspect.getmodule(frame)
+    assert module is not None and module.__file__ is not None
+    name = _removesuffix(Path(module.__file__).stem, '_documentation')
+    if name == 'overview':
+        name = ''
+    if name not in registry:
+        registry[name] = DocumentationPage(name=name)
+    return registry[name]
+
+
+def title(title_: Optional[str] = None, subtitle: Optional[str] = None) -> None:
+    """Set the title of the current documentation page."""
+    page = _get_current_page()
+    page.title = title_
+    page.subtitle = subtitle
+
+
+def text(title_: str, description: str) -> None:
+    """Add a text block to the current documentation page."""
+    _get_current_page().parts.append(DocumentationPart(title=title_, description=description))
+
+
+@overload
+def demo(title_: str,
+         description: str, /, *,
+         tab: Optional[Union[str, Callable]] = None,
+         lazy: bool = True,
+         ) -> Callable[[Callable], Callable]:
+    ...
+
+
+@overload
+def demo(element: type, /,
+         tab: Optional[Union[str, Callable]] = None,
+         lazy: bool = True,
+         ) -> Callable[[Callable], Callable]:
+    ...
+
+
+@overload
+def demo(function: Callable, /,
+         tab: Optional[Union[str, Callable]] = None,
+         lazy: bool = True,
+         ) -> Callable[[Callable], Callable]:
+    ...
+
+
+def demo(*args, **kwargs) -> Callable[[Callable], Callable]:
+    """Add a demo section to the current documentation page."""
+    if len(args) == 2:
+        element = None
+        title_, description = args
+        is_markdown = True
+    else:
+        element = args[0]
+        doc = element.__init__.__doc__ if isinstance(element, type) else element.__doc__  # type: ignore
+        title_, description = doc.split('\n', 1)
+        is_markdown = False
+
+    description = remove_indentation(description)
+    page = _get_current_page()
+
+    def decorator(function: Callable) -> Callable:
+        if not page.parts and element:
+            ui_name = _find_attribute(nicegui_ui, element.__name__)
+            app_name = _find_attribute(nicegui_app, element.__name__)
+            if ui_name:
+                page.title = f'ui.*{ui_name}*'
+            if app_name:
+                page.title = f'app.*{app_name}*'
+        page.parts.append(DocumentationPart(
+            title=title_,
+            description=description,
+            description_format='md' if is_markdown else 'rst',
+            demo=Demo(function=function, lazy=kwargs.get('lazy', True), tab=kwargs.get('tab')),
+        ))
+        return function
+    return decorator
+
+
+def ui(function: Callable) -> Callable:
+    """Add arbitrary UI to the current documentation page."""
+    _get_current_page().parts.append(DocumentationPart(ui=function))
+    return function
+
+
+def intro(documentation: types.ModuleType) -> None:
+    """Add an intro section to the current documentation page."""
+    current_page = _get_current_page()
+    target_page = get_page(documentation)
+    target_page.back_link = current_page.name
+    part = deepcopy(target_page.parts[0])
+    part.link = target_page.name
+    current_page.parts.append(part)
+
+
+def reference(element: type, *,
+              title: str = 'Reference',  # pylint: disable=redefined-outer-name
+              ) -> None:
+    """Add a reference section to the current documentation page."""
+    _get_current_page().parts.append(DocumentationPart(title=title, reference=element))
+
+
+def extra_column(function: Callable) -> Callable:
+    """Add an extra column to the current documentation page."""
+    _get_current_page().extra_column = function
+    return function
+
+
+def _find_attribute(obj: Any, name: str) -> Optional[str]:
+    for attr in dir(obj):
+        if attr.lower().replace('_', '') == name.lower().replace('_', ''):
+            return attr
+    return None
+
+
+def _removesuffix(string: str, suffix: str) -> str:
+    # NOTE: Remove this once we drop Python 3.8 support
+    if string.endswith(suffix):
+        return string[:-len(suffix)]
+    return string

+ 21 - 0
website/documentation/content/doc/page.py

@@ -0,0 +1,21 @@
+from dataclasses import dataclass, field
+from typing import Callable, List, Optional
+
+from nicegui.dataclasses import KWONLY_SLOTS
+
+from .part import DocumentationPart
+
+
+@dataclass(**KWONLY_SLOTS)
+class DocumentationPage:
+    name: str
+    title: Optional[str] = None
+    subtitle: Optional[str] = None
+    back_link: Optional[str] = None
+    parts: List[DocumentationPart] = field(default_factory=list)
+    extra_column: Optional[Callable] = None
+
+    @property
+    def heading(self) -> str:
+        """Return the heading of the page."""
+        return self.title or self.parts[0].title or ''

+ 30 - 0
website/documentation/content/doc/part.py

@@ -0,0 +1,30 @@
+
+from dataclasses import dataclass
+from typing import Callable, Literal, Optional, Union
+
+from nicegui.dataclasses import KWONLY_SLOTS
+
+from ....style import create_anchor_name
+
+
+@dataclass(**KWONLY_SLOTS)
+class Demo:
+    function: Callable
+    lazy: bool = True
+    tab: Optional[Union[str, Callable]] = None
+
+
+@dataclass(**KWONLY_SLOTS)
+class DocumentationPart:
+    title: Optional[str] = None
+    description: Optional[str] = None
+    description_format: Literal['md', 'rst'] = 'md'
+    link: Optional[str] = None
+    ui: Optional[Callable] = None
+    demo: Optional[Demo] = None
+    reference: Optional[type] = None
+
+    @property
+    def link_target(self) -> Optional[str]:
+        """Return the link target for in-page navigation."""
+        return create_anchor_name(self.title) if self.title else None

+ 15 - 0
website/documentation/content/download_documentation.py

@@ -0,0 +1,15 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.download)
+def main_demo() -> None:
+    ui.button('Logo', on_click=lambda: ui.download('https://nicegui.io/logo.png'))
+
+
+@doc.demo('Download raw bytes from memory', '''
+    The `download` function can also be used to download raw bytes from memory.
+''')
+def raw_bytes():
+    ui.button('Download', on_click=lambda: ui.download(b'Hello World', 'hello.txt'))

+ 50 - 0
website/documentation/content/echart_documentation.py

@@ -0,0 +1,50 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.echart)
+def main_demo() -> None:
+    from random import random
+
+    echart = ui.echart({
+        'xAxis': {'type': 'value'},
+        'yAxis': {'type': 'category', 'data': ['A', 'B'], 'inverse': True},
+        'legend': {'textStyle': {'color': 'gray'}},
+        'series': [
+            {'type': 'bar', 'name': 'Alpha', 'data': [0.1, 0.2]},
+            {'type': 'bar', 'name': 'Beta', 'data': [0.3, 0.4]},
+        ],
+    })
+
+    def update():
+        echart.options['series'][0]['data'][0] = random()
+        echart.update()
+
+    ui.button('Update', on_click=update)
+
+
+@doc.demo('EChart with clickable points', '''
+    You can register a callback for an event when a series point is clicked.
+''')
+def clickable_points() -> None:
+    ui.echart({
+        'xAxis': {'type': 'category'},
+        'yAxis': {'type': 'value'},
+        'series': [{'type': 'line', 'data': [20, 10, 30, 50, 40, 30]}],
+    }, on_point_click=ui.notify)
+
+
+@doc.demo('EChart with dynamic properties', '''
+    Dynamic properties can be passed to chart elements to customize them such as apply an axis label format.
+    To make a property dynamic, prefix a colon ":" to the property name.
+''')
+def dynamic_properties() -> None:
+    ui.echart({
+        'xAxis': {'type': 'category'},
+        'yAxis': {'axisLabel': {':formatter': 'value => "$" + value'}},
+        'series': [{'type': 'line', 'data': [5, 8, 13, 21, 34, 55]}],
+    })
+
+
+doc.reference(ui.echart)

+ 6 - 0
website/documentation/more/editor_documentation.py → website/documentation/content/editor_documentation.py

@@ -1,7 +1,13 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.editor)
 def main_demo() -> None:
     editor = ui.editor(placeholder='Type something here')
     ui.markdown().bind_content_from(editor, 'value',
                                     backward=lambda v: f'HTML code:\n```\n{v}\n```')
+
+
+doc.reference(ui.editor)

+ 73 - 0
website/documentation/content/element_documentation.py

@@ -0,0 +1,73 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.element)
+def main_demo() -> None:
+    with ui.element('div').classes('p-2 bg-blue-100'):
+        ui.label('inside a colored div')
+
+
+@doc.demo('Move elements', '''
+    This demo shows how to move elements between or within containers.
+''')
+def move_elements() -> None:
+    with ui.card() as a:
+        ui.label('A')
+        x = ui.label('X')
+
+    with ui.card() as b:
+        ui.label('B')
+
+    ui.button('Move X to A', on_click=lambda: x.move(a))
+    ui.button('Move X to B', on_click=lambda: x.move(b))
+    ui.button('Move X to top', on_click=lambda: x.move(target_index=0))
+
+
+@doc.demo('Default props', '''
+    You can set default props for all elements of a certain class.
+    This way you can avoid repeating the same props over and over again.
+    
+    Default props only apply to elements created after the default props were set.
+    Subclasses inherit the default props of their parent class.
+''')
+def default_props() -> None:
+    ui.button.default_props('rounded outline')
+    ui.button('Button A')
+    ui.button('Button B')
+    # END OF DEMO
+    ui.button.default_props(remove='rounded outline')
+
+
+@doc.demo('Default classes', '''
+    You can set default classes for all elements of a certain class.
+    This way you can avoid repeating the same classes over and over again.
+    
+    Default classes only apply to elements created after the default classes were set.
+    Subclasses inherit the default classes of their parent class.
+''')
+def default_classes() -> None:
+    ui.label.default_classes('bg-blue-100 p-2')
+    ui.label('Label A')
+    ui.label('Label B')
+    # END OF DEMO
+    ui.label.default_classes(remove='bg-blue-100 p-2')
+
+
+@doc.demo('Default style', '''
+    You can set a default style for all elements of a certain class.
+    This way you can avoid repeating the same style over and over again.
+    
+    A default style only applies to elements created after the default style was set.
+    Subclasses inherit the default style of their parent class.
+''')
+def default_style() -> None:
+    ui.label.default_style('color: tomato')
+    ui.label('Label A')
+    ui.label('Label B')
+    # END OF DEMO
+    ui.label.default_style(remove='color: tomato')
+
+
+doc.reference(ui.element)

+ 22 - 0
website/documentation/content/expansion_documentation.py

@@ -0,0 +1,22 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.expansion)
+def main_demo() -> None:
+    with ui.expansion('Expand!', icon='work').classes('w-full'):
+        ui.label('inside the expansion')
+
+
+@doc.demo('Expansion with Custom Header', '''
+    Instead of setting a plain-text title, you can fill the expansion header with UI elements by adding them to the "header" slot.
+''')
+def expansion_with_custom_header():
+    with ui.expansion() as expansion:
+        with expansion.add_slot('header'):
+            ui.image('https://nicegui.io/logo.png').classes('w-16')
+        ui.label('What a nice GUI!')
+
+
+doc.reference(ui.expansion)

+ 124 - 0
website/documentation/content/generic_events_documentation.py

@@ -0,0 +1,124 @@
+from nicegui import context, ui
+
+from . import doc
+
+doc.title('Generic Events')
+
+
+@doc.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 `GenericEventArguments` 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.
+
+    Here you can find more information about the events that are supported:
+
+    - https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement#events for HTML elements
+    - https://quasar.dev/vue-components for Quasar-based elements (see the "Events" tab on the individual component page)
+''')
+def generic_events_demo() -> None:
+    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(e))
+        ui.button('F').on('mousedown', lambda e: ui.notify(e), ['ctrlKey', 'shiftKey'])
+
+
+@doc.demo('Specifying event attributes', '''
+    **A list of strings** names the attributes of the JavaScript event object:
+    ```py
+    ui.button().on('click', handle_click, ['clientX', 'clientY'])
+    ```
+
+    **An empty list** requests _no_ attributes:
+    ```py
+    ui.button().on('click', handle_click, [])
+    ```
+
+    **The value `None`** represents _all_ attributes (the default):
+    ```py
+    ui.button().on('click', handle_click, None)
+    ```
+
+    **If the event is called with multiple arguments** like QTable's "row-click" `(evt, row, index) => void`,
+    you can define a list of argument definitions:
+    ```py
+    ui.table(...).on('rowClick', handle_click, [[], ['name'], None])
+    ```
+    In this example the "row-click" event will omit all arguments of the first `evt` argument,
+    send only the "name" attribute of the `row` argument and send the full `index`.
+
+    If the retrieved list of event arguments has length 1, the argument is automatically unpacked.
+    So you can write
+    ```py
+    ui.button().on('click', lambda e: print(e.args['clientX'], flush=True))
+    ```
+    instead of
+    ```py
+    ui.button().on('click', lambda e: print(e.args[0]['clientX'], flush=True))
+    ```
+
+    Note that by default all JSON-serializable attributes of all arguments are sent.
+    This is to simplify registering for new events and discovering their attributes.
+    If bandwidth is an issue, the arguments should be limited to what is actually needed on the server.
+''')
+def event_attributes() -> None:
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name'},
+        {'name': 'age', 'label': 'Age', 'field': 'age'},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 42},
+        {'name': 'Bob', 'age': 23},
+    ]
+    ui.table(columns, rows, 'name').on('rowClick', ui.notify, [[], ['name'], None])
+
+
+@doc.demo('Modifiers', '''
+    You can also include [key modifiers](https://vuejs.org/guide/essentials/event-handling.html#key-modifiers>) (shown in input "A"),
+    modifier combinations (shown in input "B"),
+    and [event modifiers](https://vuejs.org/guide/essentials/event-handling.html#mouse-button-modifiers>) (shown in input "C").
+''')
+def modifiers() -> None:
+    with ui.row():
+        ui.input('A').classes('w-12').on('keydown.space', lambda: ui.notify('You pressed space.'))
+        ui.input('B').classes('w-12').on('keydown.y.shift', lambda: ui.notify('You pressed Shift+Y'))
+        ui.input('C').classes('w-12').on('keydown.once', lambda: ui.notify('You started typing.'))
+
+
+@doc.demo('Custom events', '''
+    It is fairly easy to emit custom events from JavaScript which can be listened to with `element.on(...)`.
+    This can be useful if you want to call Python code when something happens in JavaScript.
+    In this example we are listening to the `visibilitychange` event of the browser tab.
+''')
+async def custom_events() -> None:
+    tabwatch = ui.checkbox('Watch browser tab re-entering') \
+        .on('tabvisible', lambda: ui.notify('Welcome back!') if tabwatch.value else None, args=[])
+    ui.add_head_html(f'''
+        <script>
+        document.addEventListener('visibilitychange', () => {{
+            if (document.visibilityState === 'visible')
+                getElement({tabwatch.id}).$emit('tabvisible');
+        }});
+        </script>
+    ''')
+    # END OF DEMO
+    await context.get_client().connected()
+    ui.run_javascript(f'''
+        document.addEventListener('visibilitychange', () => {{
+            if (document.visibilityState === 'visible')
+                getElement({tabwatch.id}).$emit('tabvisible');
+        }});
+    ''')

+ 6 - 0
website/documentation/more/grid_documentation.py → website/documentation/content/grid_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.grid)
 def main_demo() -> None:
     with ui.grid(columns=2):
         ui.label('Name:')
@@ -11,3 +14,6 @@ def main_demo() -> None:
 
         ui.label('Height:')
         ui.label('1.80m')
+
+
+doc.reference(ui.grid)

+ 76 - 0
website/documentation/content/highchart_documentation.py

@@ -0,0 +1,76 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.highchart)
+def main_demo() -> None:
+    from random import random
+
+    chart = ui.highchart({
+        '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'][0] = random()
+        chart.update()
+
+    ui.button('Update', on_click=update)
+
+
+@doc.demo('Chart with extra dependencies', '''
+    To use a chart type that is not included in the default dependencies, you can specify extra dependencies.
+    This demo shows a solid gauge chart.
+''')
+def extra_dependencies() -> None:
+    ui.highchart({
+        'title': False,
+        'chart': {'type': 'solidgauge'},
+        'yAxis': {
+            'min': 0,
+            'max': 1,
+        },
+        'series': [
+            {'data': [0.42]},
+        ],
+    }, extras=['solid-gauge']).classes('w-full h-64')
+
+
+@doc.demo('Chart with draggable points', '''
+    This chart allows dragging the series points.
+    You can register callbacks for the following events:
+    
+    - `on_point_click`: called when a point is clicked
+    - `on_point_drag_start`: called when a point drag starts
+    - `on_point_drag`: called when a point is dragged
+    - `on_point_drop`: called when a point is dropped
+''')
+def drag() -> None:
+    ui.highchart(
+        {
+            'title': False,
+            'plotOptions': {
+                'series': {
+                    'stickyTracking': False,
+                    'dragDrop': {'draggableY': True, 'dragPrecisionY': 1},
+                },
+            },
+            'series': [
+                {'name': 'A', 'data': [[20, 10], [30, 20], [40, 30]]},
+                {'name': 'B', 'data': [[50, 40], [60, 50], [70, 60]]},
+            ],
+        },
+        extras=['draggable-points'],
+        on_point_click=lambda e: ui.notify(f'Click: {e}'),
+        on_point_drag_start=lambda e: ui.notify(f'Drag start: {e}'),
+        on_point_drop=lambda e: ui.notify(f'Drop: {e}')
+    ).classes('w-full h-64')
+
+
+doc.reference(ui.highchart)

+ 6 - 0
website/documentation/more/html_documentation.py → website/documentation/content/html_documentation.py

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

+ 30 - 0
website/documentation/content/icon_documentation.py

@@ -0,0 +1,30 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.icon)
+def main_demo() -> None:
+    ui.icon('thumb_up', color='primary').classes('text-5xl')
+
+
+@doc.demo('Eva icons', '''
+    You can use [Eva icons](https://akveo.github.io/eva-icons/) in your app.
+''', lazy=False)
+def eva_icons():
+    ui.add_head_html('<link href="https://unpkg.com/eva-icons@1.1.3/style/eva-icons.css" rel="stylesheet">')
+
+    ui.element('i').classes('eva eva-github').classes('text-5xl')
+
+
+@doc.demo('Lottie files', '''
+    You can also use [Lottie files](https://lottiefiles.com/) with animations.
+''', lazy=False)
+def lottie():
+    ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+    src = 'https://assets5.lottiefiles.com/packages/lf20_MKCnqtNQvg.json'
+    ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-24')
+
+
+doc.reference(ui.icon)

+ 65 - 0
website/documentation/content/image_documentation.py

@@ -0,0 +1,65 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.image)
+def main_demo() -> None:
+    ui.image('https://picsum.photos/id/377/640/360')
+
+
+@doc.demo('Local files', '''
+    You can use local images as well by passing a path to the image file.
+''')
+def local():
+    ui.image('website/static/logo.png').classes('w-16')
+
+
+@doc.demo('Base64 string', '''
+    You can also use a Base64 string as image source.
+''')
+def base64():
+    base64 = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+    ui.image(base64).classes('w-2 h-2 m-auto')
+
+
+@doc.demo('PIL image', '''
+    You can also use a PIL image as image source.
+''')
+def pil():
+    import numpy as np
+    from PIL import Image
+
+    image = Image.fromarray(np.random.randint(0, 255, (100, 100), dtype=np.uint8))
+    ui.image(image).classes('w-32')
+
+
+@doc.demo('Lottie files', '''
+    You can also use [Lottie files](https://lottiefiles.com/) with animations.
+''', lazy=False)
+def lottie():
+    ui.add_body_html('<script src="https://unpkg.com/@lottiefiles/lottie-player@latest/dist/lottie-player.js"></script>')
+
+    src = 'https://assets1.lottiefiles.com/datafiles/HN7OcWNnoqje6iXIiZdWzKxvLIbfeCGTmvXmEm1h/data.json'
+    ui.html(f'<lottie-player src="{src}" loop autoplay />').classes('w-full')
+
+
+@doc.demo('Image link', '''
+    Images can link to another page by wrapping them in a [ui.link](https://nicegui.io/documentation/link).
+''')
+def link():
+    with ui.link(target='https://github.com/zauberzeug/nicegui'):
+        ui.image('https://picsum.photos/id/41/640/360').classes('w-64')
+
+
+@doc.demo('Force reload', '''
+    You can force an image to reload by calling the `force_reload` method.
+    It will append a timestamp to the image URL, which will make the browser reload the image.
+''')
+def force_reload():
+    img = ui.image('https://picsum.photos/640/360').classes('w-64')
+
+    ui.button('Force reload', on_click=img.force_reload)
+
+
+doc.reference(ui.image)

+ 45 - 0
website/documentation/content/input_documentation.py

@@ -0,0 +1,45 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.input)
+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()
+
+
+@doc.demo('Autocompletion', '''
+    The `autocomplete` feature provides suggestions as you type, making input easier and faster.
+    The parameter `options` is a list of strings that contains the available options that will appear.
+''')
+def autocomplete_demo():
+    options = ['AutoComplete', 'NiceGUI', 'Awesome']
+    ui.input(label='Text', placeholder='start typing', autocomplete=options)
+
+
+@doc.demo('Clearable', '''
+    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
+''')
+def clearable():
+    i = ui.input(value='some text').props('clearable')
+    ui.label().bind_text_from(i, 'value')
+
+
+@doc.demo('Styling', '''
+    Quasar has a lot of [props to change the appearance](https://quasar.dev/vue-components/input).
+    It is even possible to style the underlying input with `input-style` and `input-class` props
+    and use the provided slots to add custom elements.
+''')
+def styling():
+    ui.input(placeholder='start typing').props('rounded outlined dense')
+    ui.input('styling', value='some text') \
+        .props('input-style="color: blue" input-class="font-mono"')
+    with ui.input(value='custom clear button').classes('w-64') as i:
+        ui.button(color='orange-8', on_click=lambda: i.set_value(None), icon='delete') \
+            .props('flat dense').bind_visibility_from(i, 'value')
+
+
+doc.reference(ui.input)

+ 41 - 0
website/documentation/content/interactive_image_documentation.py

@@ -0,0 +1,41 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.interactive_image)
+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)
+
+
+@doc.demo('Nesting elements', '''
+    You can nest elements inside an interactive image.
+    Use Tailwind classes like "absolute top-0 left-0" to position the label absolutely with respect to the image.
+    Of course this can be done with plain CSS as well.
+''')
+def nesting_elements():
+    with ui.interactive_image('https://picsum.photos/id/147/640/360'):
+        ui.button(on_click=lambda: ui.notify('thumbs up'), icon='thumb_up') \
+            .props('flat fab color=white') \
+            .classes('absolute bottom-0 left-0 m-2')
+
+
+@doc.demo('Force reload', '''
+    You can force an image to reload by calling the `force_reload` method.
+    It will append a timestamp to the image URL, which will make the browser reload the image.
+''')
+def force_reload():
+    img = ui.interactive_image('https://picsum.photos/640/360').classes('w-64')
+
+    ui.button('Force reload', on_click=img.force_reload)
+
+
+doc.reference(ui.interactive_image)

+ 6 - 0
website/documentation/more/joystick_documentation.py → website/documentation/content/joystick_documentation.py

@@ -1,8 +1,14 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.joystick)
 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')
+
+
+doc.reference(ui.joystick)

+ 6 - 0
website/documentation/more/json_editor_documentation.py → website/documentation/content/json_editor_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.json_editor)
 def main_demo() -> None:
     json = {
         'array': [1, 2, 3],
@@ -18,3 +21,6 @@ def main_demo() -> None:
     ui.json_editor({'content': {'json': json}},
                    on_select=lambda e: ui.notify(f'Select: {e}'),
                    on_change=lambda e: ui.notify(f'Change: {e}'))
+
+
+doc.reference(ui.json_editor)

+ 6 - 0
website/documentation/more/keyboard_documentation.py → website/documentation/content/keyboard_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.keyboard)
 def main_demo() -> None:
     from nicegui.events import KeyEventArguments
 
@@ -23,3 +26,6 @@ def main_demo() -> None:
     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')
+
+
+doc.reference(ui.keyboard)

+ 6 - 0
website/documentation/more/knob_documentation.py → website/documentation/content/knob_documentation.py

@@ -1,8 +1,14 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.knob)
 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')
+
+
+doc.reference(ui.knob)

+ 29 - 0
website/documentation/content/label_documentation.py

@@ -0,0 +1,29 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.label)
+def main_demo() -> None:
+    ui.label('some label')
+
+
+@doc.demo('Change Appearance Depending on the Content', '''
+    You can overwrite the `_handle_text_change` method to update other attributes of a label depending on its content. 
+    This technique also works for bindings as shown in the example below.
+''')
+def status():
+    class status_label(ui.label):
+        def _handle_text_change(self, text: str) -> None:
+            super()._handle_text_change(text)
+            if text == 'ok':
+                self.classes(replace='text-positive')
+            else:
+                self.classes(replace='text-negative')
+
+    model = {'status': 'error'}
+    status_label().bind_text_from(model, 'status')
+    ui.switch(on_change=lambda e: model.update(status='ok' if e.value else 'error'))
+
+
+doc.reference(ui.label)

+ 6 - 0
website/documentation/more/line_plot_documentation.py → website/documentation/content/line_plot_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import events, ui
 
+from . import doc
 
+
+@doc.demo(ui.line_plot)
 def main_demo() -> None:
     import math
     from datetime import datetime
@@ -27,3 +30,6 @@ def main_demo() -> None:
         if line_checkbox.value:
             ui.timer(10.0, turn_off, once=True)
     line_checkbox.on('update:model-value', handle_change, args=[None])
+
+
+doc.reference(ui.line_plot)

+ 6 - 0
website/documentation/more/linear_progress_documentation.py → website/documentation/content/linear_progress_documentation.py

@@ -1,6 +1,12 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.linear_progress)
 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')
+
+
+doc.reference(ui.linear_progress)

+ 59 - 0
website/documentation/content/link_documentation.py

@@ -0,0 +1,59 @@
+from nicegui import ui
+
+from ...style import link_target
+from . import doc
+
+
+@doc.demo(ui.link)
+def main_demo() -> None:
+    ui.link('NiceGUI on GitHub', 'https://github.com/zauberzeug/nicegui')
+
+
+@doc.demo('Navigate on large pages', '''
+    To jump to a specific location within a page you can place linkable anchors with `ui.link_target('target_name')`
+    or simply pass a NiceGUI element as link target.
+''')
+def same_page_links():
+    navigation = ui.row()
+    # ui.link_target('target_A')
+    link_target('target_A', '-10px')  # HIDE
+    ui.label(
+        'Lorem ipsum dolor sit amet, consectetur adipiscing elit, '
+        'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
+    )
+    link_target('target_B', '70px')  # HIDE
+    label_B = ui.label(
+        'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. '
+        'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. '
+        'Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
+    )
+    with navigation:
+        ui.link('Goto A', '#target_A')
+        # ui.link('Goto B', label_B)
+        ui.link('Goto B', '#target_B')  # HIDE
+
+
+@doc.demo('Links to other pages', '''
+    You can link to other pages by providing the link target as path or function reference.
+''')
+def link_to_other_page():
+    @ui.page('/some_other_page')
+    def my_page():
+        ui.label('This is another page')
+
+    ui.label('Go to other page')
+    ui.link('... with path', '/some_other_page')
+    ui.link('... with function reference', my_page)
+
+
+@doc.demo('Link from images and other elements', '''
+    By nesting elements inside a link you can make the whole element clickable.
+    This works with all elements but is most useful for non-interactive elements like 
+    [ui.image](/documentation/image), [ui.avatar](/documentation/image) etc.
+''')
+def link_from_elements():
+    with ui.link(target='https://github.com/zauberzeug/nicegui'):
+        ui.image('https://picsum.photos/id/41/640/360').classes('w-64')
+
+
+doc.reference(ui.link)

+ 42 - 0
website/documentation/content/log_documentation.py

@@ -0,0 +1,42 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.log)
+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]))
+
+
+@doc.demo('Attach to a logger', '''
+    You can attach a `ui.log` element to a Python logger object so that log messages are pushed to the log element.
+''')
+def logger_handler():
+    import logging
+    from datetime import datetime
+
+    logger = logging.getLogger()
+
+    class LogElementHandler(logging.Handler):
+        """A logging handler that emits messages to a log element."""
+
+        def __init__(self, element: ui.log, level: int = logging.NOTSET) -> None:
+            self.element = element
+            super().__init__(level)
+
+        def emit(self, record: logging.LogRecord) -> None:
+            try:
+                msg = self.format(record)
+                self.element.push(msg)
+            except Exception:
+                self.handleError(record)
+
+    log = ui.log(max_lines=10).classes('w-full')
+    logger.addHandler(LogElementHandler(log))
+    ui.button('Log time', on_click=lambda: logger.warning(datetime.now().strftime('%X.%f')[:-5]))
+
+
+doc.reference(ui.log)

+ 58 - 0
website/documentation/content/markdown_documentation.py

@@ -0,0 +1,58 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.markdown)
+def main_demo() -> None:
+    ui.markdown('This is **Markdown**.')
+
+
+@doc.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.
+    ''')
+
+
+@doc.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)
+        ```
+    ''')
+
+
+@doc.demo('Markdown tables', '''
+    By activating the "tables" extra, you can use Markdown tables.
+    See the [markdown2 documentation](https://github.com/trentm/python-markdown2/wiki/Extras#implemented-extras) for a list of available extras.
+''')
+def markdown_tables():
+    ui.markdown('''
+        | First name | Last name |
+        | ---------- | --------- |
+        | Max        | Planck    |
+        | Marie      | Curie     |
+    ''', extras=['tables'])
+
+
+doc.reference(ui.markdown)

+ 6 - 0
website/documentation/more/menu_documentation.py → website/documentation/content/menu_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.menu)
 def main_demo() -> None:
     with ui.row().classes('w-full items-center'):
         result = ui.label().classes('mr-auto')
@@ -12,3 +15,6 @@ def main_demo() -> None:
                              lambda: result.set_text('Selected item 3'), auto_close=False)
                 ui.separator()
                 ui.menu_item('Close', on_click=menu.close)
+
+
+doc.reference(ui.menu)

+ 6 - 0
website/documentation/more/mermaid_documentation.py → website/documentation/content/mermaid_documentation.py

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

+ 33 - 0
website/documentation/content/notify_documentation.py

@@ -0,0 +1,33 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.notify)
+def main_demo() -> None:
+    ui.button('Say hi!', on_click=lambda: ui.notify('Hi!', close_button='OK'))
+
+
+@doc.demo('Notification Types', '''
+    There are different types that can be used to indicate the nature of the notification.
+''')
+def notify_colors():
+    ui.button('negative', on_click=lambda: ui.notify('error', type='negative'))
+    ui.button('positive', on_click=lambda: ui.notify('success', type='positive'))
+    ui.button('warning', on_click=lambda: ui.notify('warning', type='warning'))
+
+
+@doc.demo('Multiline Notifications', '''
+    To allow a notification text to span multiple lines, it is sufficient to set `multi_line=True`.
+    If manual newline breaks are required (e.g. `\\n`), you need to define a CSS style and pass it to the notification as shown in the example.
+''')
+def multiline():
+    ui.html('<style>.multi-line-notification { white-space: pre-line; }</style>')
+    ui.button('show', on_click=lambda: ui.notify(
+        'Lorem ipsum dolor sit amet, consectetur adipisicing elit. \n'
+        'Hic quisquam non ad sit assumenda consequuntur esse inventore officia. \n'
+        'Corrupti reiciendis impedit vel, '
+        'fugit odit quisquam quae porro exercitationem eveniet quasi.',
+        multi_line=True,
+        classes='multi-line-notification',
+    ))

+ 33 - 0
website/documentation/content/number_documentation.py

@@ -0,0 +1,33 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.number)
+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()
+
+
+@doc.demo('Clearable', '''
+    The `clearable` prop from [Quasar](https://quasar.dev/) adds a button to the input that clears the text.    
+''')
+def clearable():
+    i = ui.number(value=42).props('clearable')
+    ui.label().bind_text_from(i, 'value')
+
+
+@doc.demo('Number of decimal places', '''
+    You can specify the number of decimal places using the `precision` parameter.
+    A negative value means decimal places before the dot.
+    The rounding takes place when the input loses focus,
+    when sanitization parameters like min, max or precision change,
+    or when `sanitize()` is called manually.
+''')
+def integer():
+    n = ui.number(value=3.14159265359, precision=5)
+    n.sanitize()
+
+
+doc.reference(ui.number)

+ 9 - 0
website/documentation/content/open_documentation.py

@@ -0,0 +1,9 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.open)
+def main_demo() -> None:
+    url = 'https://github.com/zauberzeug/nicegui/'
+    ui.button('Open GitHub', on_click=lambda: ui.open(url, new_tab=True))

+ 119 - 0
website/documentation/content/overview.py

@@ -0,0 +1,119 @@
+from nicegui import ui
+
+from . import (doc, section_action_events, section_audiovisual_elements, section_binding_properties,
+               section_configuration_deployment, section_controls, section_data_elements, section_page_layout,
+               section_pages_routing, section_styling_appearance, section_text_elements)
+
+doc.title('*NiceGUI* Documentation', 'Reference, Demos and more')
+
+doc.text('Overview', '''
+    NiceGUI is an open-source Python library to write graphical user interfaces which run in the browser.
+    It has a very gentle learning curve while still offering the option for advanced customizations.
+    NiceGUI follows a backend-first philosophy:
+    It handles all the web development details.
+    You can focus on writing Python code. 
+    This makes it ideal for a wide range of projects including short 
+    scripts, dashboards, robotics projects, IoT solutions, smart home automation, and machine learning.
+''')
+
+doc.text('How to use this guide', '''
+    This documentation explains how to use NiceGUI.
+    Each of the tiles covers a NiceGUI topic in detail.
+    It is recommended to start by reading this entire introduction page, then refer to other sections as needed.
+''')
+
+doc.text('Basic concepts', '''
+    NiceGUI provides UI _components_ (or _elements_) such as buttons, sliders, text, images, charts, and more.
+    Your app assembles these components into _pages_.
+    When the user interacts with an item on a page, NiceGUI triggers an _event_ (or _action_).
+    You define code to _handle_ each event, such as what to do when a user clicks a button named "Go".
+
+    Components are arranged on a page using _layouts_.
+    Layouts provide things like grids, tabs, carousels, expansions, menus, and other tools to arrange your components.
+    Many components are linked to a _model_ (data object) in your code, which automatically updates the user interface when the value changes.
+
+    Styling and appearance can be controlled in several ways.
+    NiceGUI accepts optional arguments for certain styling, such as icons on buttons.
+    Other styling can be set with functions such as `.styles`, `.classes`, or `.props` that you'll learn about later.
+    Global styles like colors and fonts can be set with dedicated properties.
+    Or if you prefer, almost anything can be styled with CSS.
+''')
+
+doc.text('Actions', '''
+    NiceGUI runs an event loop to handle user input and other events like timers and keyboard bindings.
+    You can write asynchronous functions for long-running tasks to keep the UI responsive.
+    The _Actions_ section covers how to work with events.
+''')
+
+doc.text('Implementation', '''
+    NiceGUI is implemented with HTML components served by an HTTP server (FastAPI), even for native windows.
+    If you already know HTML, everything will feel very familiar.
+    If you don't know HTML, that's fine too!
+    NiceGUI abstracts away the details, so you can focus on creating beautiful interfaces without worrying about how they are implemented.
+''')
+
+doc.text('Running NiceGUI Apps', '''
+    There are several options for deploying NiceGUI.
+    By default, NiceGUI runs a server on localhost and runs your app as a private web page on the local machine.
+    When run this way, your app appears in a web browser window.
+    You can also run NiceGUI in a native window separate from a web browser.
+    Or you can run NiceGUI on a server that handles many clients - the website you're reading right now is served from NiceGUI.
+
+    After creating your app pages with components, you call `ui.run()` to start the NiceGUI server.
+    Optional parameters to `ui.run` set things like the network address and port the server binds to, 
+    whether the app runs in native mode, initial window size, and many other options.
+    The section _Configuration and Deployment_ covers the options to the `ui.run()` function and the FastAPI framework it is based on.
+''')
+
+doc.text('Customization', '''
+    If you want more customization in your app, you can use the underlying Tailwind classes and Quasar components
+    to control the style or behavior of your components.
+    You can also extend the available components by subclassing existing NiceGUI components or importing new ones from Quasar.
+    All of this is optional.
+    Out of the box, NiceGUI provides everything you need to make modern, stylish, responsive user interfaces.
+''')
+
+tiles = [
+    (section_text_elements, '''
+        Elements like `ui.label`, `ui.markdown` and `ui.html` can be used to display text and other content.
+    '''),
+    (section_controls, '''
+        NiceGUI provides a variety of elements for user interaction, e.g. `ui.button`, `ui.slider`, `ui.inputs`, etc.
+    '''),
+    (section_audiovisual_elements, '''
+        You can use elements like `ui.image`, `ui.audio`, `ui.video`, etc. to display audiovisual content.
+    '''),
+    (section_data_elements, '''
+        There are several elements for displaying data, e.g. `ui.table`, `ui.aggrid`, `ui.highchart`, `ui.echart`, etc.
+    '''),
+    (section_binding_properties, '''
+        To update UI elements automatically, you can bind them to each other or to your data model.
+    '''),
+    (section_page_layout, '''
+        This section covers fundamental techniques as well as several elements to structure your UI.
+    '''),
+    (section_styling_appearance, '''
+        NiceGUI allows to customize the appearance of UI elements in various ways, including CSS, Tailwind CSS and Quasar properties.
+    '''),
+    (section_action_events, '''
+        This section covers timers, UI events, and the lifecycle of NiceGUI apps.
+    '''),
+    (section_pages_routing, '''
+        A NiceGUI app can consist of multiple pages and other FastAPI endpoints.
+    '''),
+    (section_configuration_deployment, '''
+        Whether you want to run your app locally or on a server, native or in a browser, we got you covered.
+    '''),
+]
+
+
+@doc.extra_column
+def create_tiles():
+    for documentation, description in tiles:
+        page = doc.get_page(documentation)
+        with ui.link(target=f'/documentation/{page.name}') \
+                .classes('bg-[#5898d420] p-4 self-stretch rounded flex flex-col gap-2') \
+                .style('box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)'):
+            if page.title:
+                ui.label(page.title.replace('*', '')).classes(replace='text-2xl')
+            ui.markdown(description).classes(replace='bold-links arrow-links')

+ 86 - 0
website/documentation/content/page_documentation.py

@@ -0,0 +1,86 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.page)
+def main_demo() -> None:
+    @ui.page('/other_page')
+    def other_page():
+        ui.label('Welcome to the other side')
+
+    @ui.page('/dark_page', dark=True)
+    def dark_page():
+        ui.label('Welcome to the dark side')
+
+    ui.link('Visit other page', other_page)
+    ui.link('Visit dark page', dark_page)
+
+
+@doc.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')
+
+
+@doc.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)
+
+
+@doc.demo('Modularize with APIRouter', '''
+    You can use the NiceGUI specialization of
+    [FastAPI's APIRouter](https://fastapi.tiangolo.com/tutorial/bigger-applications/?h=apirouter#apirouter)
+    to modularize your code by grouping pages and other routes together.
+    This is especially useful if you want to reuse the same prefix for multiple pages.
+    The router and its pages can be neatly tugged away in a separate module (e.g. file) and
+    the router is simply imported and included in the main app.
+    See our [modularization example](https://github.com/zauberzeug/nicegui/blob/main/examples/modularization/example_c.py)
+    for a multi-file app structure.
+''', tab='/sub-path')
+def api_router_demo():
+    # from nicegui import APIRouter, app
+    #
+    # router = APIRouter(prefix='/sub-path')
+    #
+    # @router.page('/')
+    # def page():
+    #     ui.label('This is content on /sub-path')
+    #
+    # @router.page('/sub-sub-path')
+    # def page():
+    #     ui.label('This is content on /sub-path/sub-sub-path')
+    #
+    # ui.link('Visit sub-path', '/sub-path')
+    # ui.link('Visit sub-sub-path', '/sub-path/sub-sub-path')
+    #
+    # app.include_router(router)
+    # END OF DEMO
+    ui.label('Shows up on /sub-path')

+ 6 - 0
website/documentation/more/pagination_documentation.py → website/documentation/content/pagination_documentation.py

@@ -1,6 +1,12 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.pagination)
 def main_demo() -> None:
     p = ui.pagination(1, 5, direction_links=True)
     ui.label().bind_text_from(p, 'value', lambda v: f'Page {v}')
+
+
+doc.reference(ui.pagination)

+ 70 - 0
website/documentation/content/plotly_documentation.py

@@ -0,0 +1,70 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.plotly)
+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')
+
+
+@doc.demo('Dictionary interface', '''
+    This demo shows how to use the declarative dictionary interface to create a plot.
+    For plots with many traces and data points, this is more efficient than the object-oriented interface.
+    The definition corresponds to the [JavaScript Plotly API](https://plotly.com/javascript/).
+    Due to different defaults, the resulting plot may look slightly different from the same plot created with the object-oriented interface,
+    but the functionality is the same.
+''')
+def plot_dict_interface():
+    fig = {
+        'data': [
+            {
+                'type': 'scatter',
+                'name': 'Trace 1',
+                'x': [1, 2, 3, 4],
+                'y': [1, 2, 3, 2.5],
+            },
+            {
+                'type': 'scatter',
+                'name': 'Trace 2',
+                'x': [1, 2, 3, 4],
+                'y': [1.4, 1.8, 3.8, 3.2],
+                'line': {'dash': 'dot', 'width': 3},
+            },
+        ],
+        'layout': {
+            'margin': {'l': 15, 'r': 0, 't': 0, 'b': 15},
+            'plot_bgcolor': '#E5ECF6',
+            'xaxis': {'gridcolor': 'white'},
+            'yaxis': {'gridcolor': 'white'},
+        },
+    }
+    ui.plotly(fig).classes('w-full h-40')
+
+
+@doc.demo('Plot updates', '''
+    This demo shows how to update the plot in real time.
+    Click the button to add a new trace to the plot.
+    To send the new plot to the browser, make sure to explicitly call `plot.update()` or `ui.update(plot)`.
+''')
+def plot_updates():
+    from random import random
+
+    import plotly.graph_objects as go
+
+    fig = go.Figure()
+    fig.update_layout(margin=dict(l=0, r=0, t=0, b=0))
+    plot = ui.plotly(fig).classes('w-full h-40')
+
+    def add_trace():
+        fig.add_trace(go.Scatter(x=[1, 2, 3], y=[random(), random(), random()]))
+        plot.update()
+
+    ui.button('Add trace', on_click=add_trace)
+
+
+doc.reference(ui.plotly)

+ 6 - 0
website/documentation/more/pyplot_documentation.py → website/documentation/content/pyplot_documentation.py

@@ -1,6 +1,9 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.pyplot)
 def main_demo() -> None:
     import numpy as np
     from matplotlib import pyplot as plt
@@ -9,3 +12,6 @@ def main_demo() -> None:
         x = np.linspace(0.0, 5.0)
         y = np.cos(2 * np.pi * x) * np.exp(-x)
         plt.plot(x, y, '-')
+
+
+doc.reference(ui.pyplot)

+ 38 - 0
website/documentation/content/query_documentation.py

@@ -0,0 +1,38 @@
+from nicegui import context, ui
+
+from . import doc
+
+
+@doc.demo(ui.query)
+def main_demo() -> None:
+    def set_background(color: str) -> None:
+        ui.query('body').style(f'background-color: {color}')
+
+    # ui.button('Blue', on_click=lambda: set_background('#ddeeff'))
+    # ui.button('Orange', on_click=lambda: set_background('#ffeedd'))
+    # END OF DEMO
+    ui.button('Blue', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ddeeff'))
+    ui.button('Orange', on_click=lambda e: e.sender.parent_slot.parent.style('background-color: #ffeedd'))
+
+
+@doc.demo('Set background gradient', '''
+    It's easy to set a background gradient, image or similar. 
+    See [w3schools.com](https://www.w3schools.com/cssref/pr_background-image.php) for more information about setting background with CSS.
+''')
+def background_image():
+    # ui.query('body').classes('bg-gradient-to-t from-blue-400 to-blue-100')
+    # END OF DEMO
+    context.get_slot_stack()[-1].parent.classes('bg-gradient-to-t from-blue-400 to-blue-100')
+
+
+@doc.demo('Modify default page padding', '''
+    By default, NiceGUI provides a built-in padding around the content of the page.
+    You can modify it using the class selector `.nicegui-content`.
+''')
+def remove_padding():
+    # ui.query('.nicegui-content').classes('p-0')
+    context.get_slot_stack()[-1].parent.classes(remove='p-4')  # HIDE
+    # with ui.column().classes('h-screen w-full bg-gray-400 justify-between'):
+    with ui.column().classes('h-full w-full bg-gray-400 justify-between'):  # HIDE
+        ui.label('top left')
+        ui.label('bottom right').classes('self-end')

+ 6 - 0
website/documentation/more/radio_documentation.py → website/documentation/content/radio_documentation.py

@@ -1,6 +1,12 @@
 from nicegui import ui
 
+from . import doc
 
+
+@doc.demo(ui.radio)
 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')
+
+
+doc.reference(ui.radio)

+ 94 - 0
website/documentation/content/refreshable_documentation.py

@@ -0,0 +1,94 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.refreshable)
+def main_demo() -> None:
+    import random
+
+    numbers = []
+
+    @ui.refreshable
+    def number_ui() -> None:
+        ui.label(', '.join(str(n) for n in sorted(numbers)))
+
+    def add_number() -> None:
+        numbers.append(random.randint(0, 100))
+        number_ui.refresh()
+
+    number_ui()
+    ui.button('Add random number', on_click=add_number)
+
+
+@doc.demo('Refreshable UI with parameters', '''
+    Here is a demo of how to use the refreshable decorator to create a UI that can be refreshed with different parameters.
+''')
+def refreshable_with_parameters():
+    from datetime import datetime
+
+    import pytz
+
+    @ui.refreshable
+    def clock_ui(timezone: str):
+        ui.label(f'Current time in {timezone}:')
+        ui.label(datetime.now(tz=pytz.timezone(timezone)).strftime('%H:%M:%S'))
+
+    clock_ui('Europe/Berlin')
+    ui.button('Refresh', on_click=clock_ui.refresh)
+    ui.button('Refresh for New York', on_click=lambda: clock_ui.refresh('America/New_York'))
+    ui.button('Refresh for Tokyo', on_click=lambda: clock_ui.refresh('Asia/Tokyo'))
+
+
+@doc.demo('Refreshable UI for input validation', '''
+    Here is a demo of how to use the refreshable decorator to give feedback about the validity of user input.
+''')
+def input_validation():
+    import re
+
+    pwd = ui.input('Password', password=True, on_change=lambda: show_info.refresh())
+
+    rules = {
+        'Lowercase letter': lambda s: re.search(r'[a-z]', s),
+        'Uppercase letter': lambda s: re.search(r'[A-Z]', s),
+        'Digit': lambda s: re.search(r'\d', s),
+        'Special character': lambda s: re.search(r"[!@#$%^&*(),.?':{}|<>]", s),
+        'min. 8 characters': lambda s: len(s) >= 8,
+    }
+
+    @ui.refreshable
+    def show_info():
+        for rule, check in rules.items():
+            with ui.row().classes('items-center gap-2'):
+                if check(pwd.value or ''):
+                    ui.icon('done', color='green')
+                    ui.label(rule).classes('text-xs text-green strike-through')
+                else:
+                    ui.icon('radio_button_unchecked', color='red')
+                    ui.label(rule).classes('text-xs text-red')
+
+    show_info()
+
+
+@doc.demo('Refreshable UI with reactive state', '''
+    You can create reactive state variables with the `ui.state` function, like `count` and `color` in this demo.
+    They can be used like normal variables for creating UI elements like the `ui.label`.
+    Their corresponding setter functions can be used to set new values, which will automatically refresh the UI.
+''')
+def reactive_state():
+    @ui.refreshable
+    def counter(name: str):
+        with ui.card():
+            count, set_count = ui.state(0)
+            color, set_color = ui.state('black')
+            ui.label(f'{name} = {count}').classes(f'text-{color}')
+            ui.button(f'{name} += 1', on_click=lambda: set_count(count + 1))
+            ui.select(['black', 'red', 'green', 'blue'],
+                      value=color, on_change=lambda e: set_color(e.value))
+
+    with ui.row():
+        counter('A')
+        counter('B')
+
+
+doc.reference(ui.refreshable)

+ 6 - 0
website/documentation/more/row_documentation.py → website/documentation/content/row_documentation.py

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

+ 82 - 0
website/documentation/content/run_documentation.py

@@ -0,0 +1,82 @@
+from nicegui import ui
+
+from . import doc
+
+doc.title('ui.*run*')
+
+
+@doc.demo(ui.run, tab='My App')
+def demo() -> None:
+    ui.label('page with custom title')
+
+    # ui.run(title='My App')
+
+
+@doc.demo('Emoji favicon', '''
+    You can use an emoji as favicon.
+    This works in Chrome, Firefox and Safari.
+''', tab=lambda: ui.markdown('🚀&nbsp; NiceGUI'))
+def emoji_favicon():
+    ui.label('NiceGUI rocks!')
+
+    # ui.run(favicon='🚀')
+
+
+@doc.demo(
+    'Base64 favicon', '''
+    You can also use an base64-encoded image as favicon.
+''', tab=lambda: (
+        ui.image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==')
+        .classes('w-4 h-4'),
+        ui.label('NiceGUI'),
+    ),
+)
+def base64_favicon():
+    ui.label('NiceGUI with a red dot!')
+
+    icon = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='
+
+    # ui.run(favicon=icon)
+
+
+@doc.demo('SVG favicon', '''
+    And directly use an SVG as favicon.
+    Works in Chrome, Firefox and Safari.
+''', tab=lambda: (
+    ui.html('''
+        <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+            <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+            <circle cx="80" cy="85" r="8" />
+            <circle cx="120" cy="85" r="8" />
+            <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+        </svg>
+    ''').classes('w-4 h-4'),
+    ui.label('NiceGUI'),
+))
+def svg_favicon():
+    ui.label('NiceGUI makes you smile!')
+
+    smiley = '''
+        <svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
+            <circle cx="100" cy="100" r="78" fill="#ffde34" stroke="black" stroke-width="3" />
+            <circle cx="80" cy="85" r="8" />
+            <circle cx="120" cy="85" r="8" />
+            <path d="m60,120 C75,150 125,150 140,120" style="fill:none; stroke:black; stroke-width:8; stroke-linecap:round" />
+        </svg>
+    '''
+
+    # ui.run(favicon=smiley)
+
+
+@doc.demo('Custom welcome message', '''
+    You can mute the default welcome message on the command line setting the `show_welcome_message` to `False`.
+    Instead you can print your own welcome message with a custom startup handler.
+''')
+def custom_welcome_message():
+    from nicegui import app
+
+    ui.label('App with custom welcome message')
+    #
+    # app.on_startup(lambda: print('Visit your app on one of these URLs:', app.urls))
+    #
+    # ui.run(show_welcome_message=False)

+ 51 - 0
website/documentation/content/run_javascript_documentation.py

@@ -0,0 +1,51 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.run_javascript)
+def main_demo() -> None:
+    def alert():
+        ui.run_javascript('alert("Hello!")')
+
+    async def get_date():
+        time = await ui.run_javascript('Date()')
+        ui.notify(f'Browser time: {time}')
+
+    def access_elements():
+        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()
+
+
+@doc.demo('Run async JavaScript', '''
+    Using `run_javascript` you can also run asynchronous code in the browser.
+    The following demo shows how to get the current location of the user.
+''')
+def run_async_javascript():
+    async def show_location():
+        response = await ui.run_javascript('''
+            return await new Promise((resolve, reject) => {
+                if (!navigator.geolocation) {
+                    reject(new Error('Geolocation is not supported by your browser'));
+                } else {
+                    navigator.geolocation.getCurrentPosition(
+                        (position) => {
+                            resolve({
+                                latitude: position.coords.latitude,
+                                longitude: position.coords.longitude,
+                            });
+                        },
+                        () => {
+                            reject(new Error('Unable to retrieve your location'));
+                        }
+                    );
+                }
+            });
+        ''', timeout=5.0)
+        ui.notify(f'Your location is {response["latitude"]}, {response["longitude"]}')
+
+    ui.button('Show location', on_click=show_location)

+ 102 - 0
website/documentation/content/scene_documentation.py

@@ -0,0 +1,102 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.scene)
+def main_demo() -> None:
+    with ui.scene().classes('w-full h-64') 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)
+
+
+@doc.demo('Handling Click Events', '''
+    You can use the `on_click` argument to `ui.scene` to handle click events.
+    The callback receives a `SceneClickEventArguments` object with the following attributes:
+
+    - `click_type`: the type of click ("click" or "dblclick").
+    - `button`: the button that was clicked (1, 2, or 3).
+    - `alt`, `ctrl`, `meta`, `shift`: whether the alt, ctrl, meta, or shift key was pressed.
+    - `hits`: a list of `SceneClickEventHit` objects, sorted by distance from the camera.
+
+    The `SceneClickEventHit` object has the following attributes:
+
+    - `object_id`: the id of the object that was clicked.
+    - `object_name`: the name of the object that was clicked.
+    - `x`, `y`, `z`: the x, y and z coordinates of the click.
+''')
+def click_events() -> None:
+    from nicegui import events
+
+    def handle_click(e: events.SceneClickEventArguments):
+        hit = e.hits[0]
+        name = hit.object_name or hit.object_id
+        ui.notify(f'You clicked on the {name} at ({hit.x:.2f}, {hit.y:.2f}, {hit.z:.2f})')
+
+    with ui.scene(width=285, height=220, on_click=handle_click) as scene:
+        scene.sphere().move(x=-1, z=1).with_name('sphere')
+        scene.box().move(x=1, z=1).with_name('box')
+
+
+@doc.demo('Draggable objects', '''
+    You can make objects draggable using the `.draggable` method.
+    There is an optional `on_drag_start` and `on_drag_end` argument to `ui.scene` to handle drag events.
+    The callbacks receive a `SceneDragEventArguments` object with the following attributes:
+    
+    - `type`: the type of drag event ("dragstart" or "dragend").
+    - `object_id`: the id of the object that was dragged.
+    - `object_name`: the name of the object that was dragged.
+    - `x`, `y`, `z`: the x, y and z coordinates of the dragged object.
+        
+    You can also use the `drag_constraints` argument to set comma-separated JavaScript expressions
+    for constraining positions of dragged objects.
+''')
+def draggable_objects() -> None:
+    from nicegui import events
+
+    def handle_drag(e: events.SceneDragEventArguments):
+        ui.notify(f'You dropped the sphere at ({e.x:.2f}, {e.y:.2f}, {e.z:.2f})')
+
+    with ui.scene(width=285, height=220,
+                  drag_constraints='z = 1', on_drag_end=handle_drag) as scene:
+        sphere = scene.sphere().move(z=1).draggable()
+
+    ui.switch('draggable sphere',
+              value=sphere.draggable_,
+              on_change=lambda e: sphere.draggable(e.value))
+
+
+@doc.demo('Rendering point clouds', '''
+    You can render point clouds using the `point_cloud` method.
+    The `points` argument is a list of point coordinates, and the `colors` argument is a list of RGB colors (0..1).
+''')
+def point_clouds() -> None:
+    import numpy as np
+
+    with ui.scene().classes('w-full h-64') as scene:
+        x, y = np.meshgrid(np.linspace(-3, 3), np.linspace(-3, 3))
+        z = np.sin(x) * np.cos(y) + 1
+        points = np.dstack([x, y, z]).reshape(-1, 3)
+        scene.point_cloud(points=points, colors=points, point_size=0.1)
+
+
+doc.reference(ui.scene)

+ 48 - 0
website/documentation/content/scroll_area_documentation.py

@@ -0,0 +1,48 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.scroll_area)
+def main_demo() -> None:
+    with ui.row():
+        with ui.scroll_area().classes('w-32 h-32 border'):
+            ui.label('I scroll. ' * 20)
+        with ui.column().classes('p-4 w-32 h-32 border'):
+            ui.label('I will not scroll. ' * 10)
+
+
+@doc.demo('Handling Scroll Events', '''
+    You can use the `on_scroll` argument in `ui.scroll_area` to handle scroll events.
+    The callback receives a `ScrollEventArguments` object with the following attributes:
+
+    - `sender`: the scroll area that generated the event
+    - `client`: the matching client
+    - additional arguments as described in [Quasar's documentation for the ScrollArea API](https://quasar.dev/vue-components/scroll-area/#qscrollarea-api)
+''')
+def scroll_events():
+    position = ui.number('scroll position:').props('readonly')
+    with ui.card().classes('w-32 h-32'):
+        with ui.scroll_area(on_scroll=lambda e: position.set_value(e.vertical_percentage)):
+            ui.label('I scroll. ' * 20)
+
+
+@doc.demo('Setting the scroll position', '''
+    You can use `scroll_to` to programmatically set the scroll position.
+    This can be useful for navigation or synchronization of multiple scroll areas.
+''')
+def scroll_position():
+    ui.number('position', value=0, min=0, max=1, step=0.1,
+              on_change=lambda e: area1.scroll_to(percent=e.value)).classes('w-32')
+
+    with ui.row():
+        with ui.card().classes('w-32 h-48'):
+            with ui.scroll_area(on_scroll=lambda e: area2.scroll_to(percent=e.vertical_percentage)) as area1:
+                ui.label('I scroll. ' * 20)
+
+        with ui.card().classes('w-32 h-48'):
+            with ui.scroll_area() as area2:
+                ui.label('I scroll. ' * 20)
+
+
+doc.reference(ui.scroll_area)

+ 152 - 0
website/documentation/content/section_action_events.py

@@ -0,0 +1,152 @@
+from nicegui import app, ui
+
+from . import (doc, generic_events_documentation, keyboard_documentation, refreshable_documentation,
+               run_javascript_documentation, storage_documentation, timer_documentation)
+
+doc.title('Action & *Events*')
+
+doc.intro(timer_documentation)
+doc.intro(keyboard_documentation)
+
+
+@doc.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.echart`, where it is difficult to automatically detect changes in the `options` dictionary.
+''')
+def ui_updates_demo():
+    from random import random
+
+    chart = ui.echart({
+        'xAxis': {'type': 'value'},
+        'yAxis': {'type': 'value'},
+        'series': [{'type': 'line', 'data': [[0, 0], [1, 1]]}],
+    })
+
+    def add():
+        chart.options['series'][0]['data'].append([random(), random()])
+        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)
+
+
+doc.intro(refreshable_documentation)
+
+
+@doc.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)
+
+
+doc.intro(generic_events_documentation)
+
+
+@doc.demo('Running CPU-bound tasks', '''
+    NiceGUI provides a `cpu_bound` function for running CPU-bound tasks in a separate process.
+    This is useful for long-running computations that would otherwise block the event loop and make the UI unresponsive.
+    The function returns a future that can be awaited.
+''')
+def cpu_bound_demo():
+    import time
+
+    from nicegui import run
+
+    def compute_sum(a: float, b: float) -> float:
+        time.sleep(1)  # simulate a long-running computation
+        return a + b
+
+    async def handle_click():
+        result = await run.cpu_bound(compute_sum, 1, 2)
+        ui.notify(f'Sum is {result}')
+
+    # ui.button('Compute', on_click=handle_click)
+    # END OF DEMO
+    async def mock_click():
+        import asyncio
+        await asyncio.sleep(1)
+        ui.notify('Sum is 3')
+    ui.button('Compute', on_click=mock_click)
+
+
+@doc.demo('Running I/O-bound tasks', '''
+    NiceGUI provides an `io_bound` function for running I/O-bound tasks in a separate thread.
+    This is useful for long-running I/O operations that would otherwise block the event loop and make the UI unresponsive.
+    The function returns a future that can be awaited.
+''')
+def io_bound_demo():
+    import requests
+
+    from nicegui import run
+
+    async def handle_click():
+        URL = 'https://httpbin.org/delay/1'
+        response = await run.io_bound(requests.get, URL, timeout=3)
+        ui.notify(f'Downloaded {len(response.content)} bytes')
+
+    ui.button('Download', on_click=handle_click)
+
+
+doc.intro(run_javascript_documentation)
+
+
+@doc.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()
+
+
+@doc.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!'))
+
+
+doc.intro(storage_documentation)

+ 47 - 0
website/documentation/content/section_audiovisual_elements.py

@@ -0,0 +1,47 @@
+from nicegui import ui
+
+from . import (audio_documentation, avatar_documentation, doc, icon_documentation, image_documentation,
+               interactive_image_documentation, video_documentation)
+
+doc.title('*Audiovisual* Elements')
+
+doc.intro(image_documentation)
+
+
+@doc.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')
+
+
+doc.intro(interactive_image_documentation)
+doc.intro(audio_documentation)
+doc.intro(video_documentation)
+doc.intro(icon_documentation)
+doc.intro(avatar_documentation)
+
+
+@doc.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)

+ 70 - 0
website/documentation/content/section_binding_properties.py

@@ -0,0 +1,70 @@
+from nicegui import ui
+
+from . import doc
+
+date = '2023-01-01'
+
+doc.title('*Binding* Properties')
+
+
+@doc.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')
+
+
+@doc.demo('Bind to dictionary', '''
+    Here we are binding the text of labels to a dictionary.
+''')
+def bind_dictionary():
+    data = {'name': 'Bob', 'age': 17}
+
+    ui.label().bind_text_from(data, 'name', backward=lambda n: f'Name: {n}')
+    ui.label().bind_text_from(data, 'age', backward=lambda a: f'Age: {a}')
+
+    ui.button('Turn 18', on_click=lambda: data.update(age=18))
+
+
+@doc.demo('Bind to variable', '''
+    Here we are binding the value from the datepicker to a bare variable.
+    Therefore we use the dictionary `globals()` which contains all global variables.
+    This demo is based on the [official datepicker example](/documentation/date#input_element_with_date_picker).
+''')
+def bind_variable():
+    # date = '2023-01-01'
+
+    with ui.input('Date').bind_value(globals(), 'date') as date_input:
+        with ui.menu() as menu:
+            ui.date(on_change=lambda: ui.notify(f'Date: {date}')).bind_value(date_input)
+        with date_input.add_slot('append'):
+            ui.icon('edit_calendar').on('click', menu.open).classes('cursor-pointer')
+
+
+@doc.demo('Bind to storage', '''
+    Bindings also work with [`app.storage`](/documentation/storage).
+    Here we are storing the value of a textarea between visits.
+    The note is also shared between all tabs of the same user.
+''')
+def ui_state():
+    from nicegui import app
+
+    # @ui.page('/')
+    # def index():
+    #     ui.textarea('This note is kept between visits')
+    #         .classes('w-full').bind_value(app.storage.user, 'note')
+    # END OF DEMO
+    ui.textarea('This note is kept between visits').classes('w-full').bind_value(app.storage.user, 'note')

+ 283 - 0
website/documentation/content/section_configuration_deployment.py

@@ -0,0 +1,283 @@
+from nicegui import ui
+
+from ..windows import bash_window, python_window
+from . import doc, run_documentation
+
+doc.title('Configuration & Deployment')
+
+
+@doc.demo('URLs', '''
+    You can access the list of all URLs on which the NiceGUI app is available via `app.urls`.
+    The URLs are not available in `app.on_startup` because the server is not yet running.
+    Instead, you can access them in a page function or register a callback with `app.urls.on_change`.
+''')
+def urls_demo():
+    from nicegui import app
+
+    # @ui.page('/')
+    # def index():
+    #     for url in app.urls:
+    #         ui.link(url, target=url)
+    # END OF DEMO
+    ui.link('https://nicegui.io', target='https://nicegui.io')
+
+
+doc.intro(run_documentation)
+
+
+@doc.demo('Native Mode', '''
+    You can enable native mode for NiceGUI by specifying `native=True` in the `ui.run` function.
+    To customize the initial window size and display mode, use the `window_size` and `fullscreen` parameters respectively.
+    Additionally, you can provide extra keyword arguments via `app.native.window_args` and `app.native.start_args`.
+    Pick any parameter as it is defined by the internally used [pywebview module](https://pywebview.flowrl.com/guide/api.html)
+    for the `webview.create_window` and `webview.start` functions.
+    Note that these keyword arguments will take precedence over the parameters defined in `ui.run`.
+
+    In native mode the `app.native.main_window` object allows you to access the underlying window.
+    It is an async version of [`Window` from pywebview](https://pywebview.flowrl.com/guide/api.html#window-object).
+''', tab=lambda: ui.label('NiceGUI'))
+def native_mode_demo():
+    from nicegui import app
+
+    app.native.window_args['resizable'] = False
+    app.native.start_args['debug'] = True
+
+    ui.label('app running in native mode')
+    # ui.button('enlarge', on_click=lambda: app.native.main_window.resize(1000, 700))
+    #
+    # ui.run(native=True, window_size=(400, 300), fullscreen=False)
+    # END OF DEMO
+    ui.button('enlarge', on_click=lambda: ui.notify('window will be set to 1000x700 in native mode'))
+
+
+# Show a helpful workaround until issue is fixed upstream.
+# For more info see: https://github.com/r0x0r/pywebview/issues/1078
+doc.text('', '''
+    If webview has trouble finding required libraries, you may get an error relating to "WebView2Loader.dll".
+    To work around this issue, try moving the DLL file up a directory, e.g.:
+    
+    * from `.venv/Lib/site-packages/webview/lib/x64/WebView2Loader.dll`
+    * to `.venv/Lib/site-packages/webview/lib/WebView2Loader.dll`
+''')
+
+
+@doc.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.
+    - `NICEGUI_STORAGE_PATH` (default: local ".nicegui") can be set to change the location of the storage files.
+    - `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}')
+
+
+doc.text('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:
+''')
+
+
+@doc.ui
+def docker_run():
+    with bash_window(classes='max-w-lg w-full h-44'):
+        ui.markdown('''
+            ```bash
+            docker run -it --restart always \\
+            -p 80:8080 \\
+            -e PUID=$(id -u) \\
+            -e PGID=$(id -g) \\
+            -v $(pwd)/:/app/ \\
+            zauberzeug/nicegui:latest
+            ```
+        ''')
+
+
+doc.text('', '''
+    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:
+''')
+
+
+@doc.ui
+def docker_compose():
+    with python_window('docker-compose.yml', classes='max-w-lg w-full h-60'):
+        ui.markdown('''
+            ```yaml
+            app:
+                image: zauberzeug/nicegui:latest
+                restart: always
+                ports:
+                    - 80:8080
+                environment:
+                    - PUID=1000 # change this to your user id
+                    - PGID=1000 # change this to your group id
+                volumes:
+                    - ./:/app/
+            ```
+        ''')
+
+
+doc.text('', '''
+    There are other handy features in the Docker image like non-root user execution and signal pass-through.
+    For more details we recommend to have a look at our [Docker example](https://github.com/zauberzeug/nicegui/tree/main/examples/docker_image).
+
+    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 development [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.
+''')
+
+doc.text('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:
+''')
+
+
+@doc.ui
+def pyinstaller():
+    with ui.row().classes('w-full items-stretch'):
+        with python_window(classes='max-w-lg w-full'):
+            ui.markdown('''
+                ```python
+                from nicegui import native, ui
+
+                ui.label('Hello from PyInstaller')
+
+                ui.run(reload=False, port=native.find_open_port())
+                ```
+            ''')
+        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 = [
+                    'python',
+                    '-m', 'PyInstaller',
+                    'main.py', # your main file with ui.run()
+                    '--name', 'myapp', # name of your app
+                    '--onefile',
+                    #'--windowed', # prevent console appearing, only use with ui.run(native=True, ...)
+                    '--add-data', f'{Path(nicegui.__file__).parent}{os.pathsep}nicegui'
+                ]
+                subprocess.call(cmd)
+                ```
+            ''')
+
+
+doc.text('', '''
+    **Packaging Tips:**
+
+    - When building a PyInstaller app, your main script can use a native window (rather than a browser window) by
+    using `ui.run(reload=False, native=True)`.
+    The `native` parameter can be `True` or `False` depending on whether you want a native window or to launch a
+    page in the user's browser - either will work in the PyInstaller generated app.
+
+    - Specifying `--windowed` to PyInstaller will prevent a terminal console from appearing.
+    However you should only use this option if you have also specified `native=True` in your `ui.run` command.
+    Without a terminal console the user won't be able to exit the app by pressing Ctrl-C.
+    With the `native=True` option, the app will automatically close when the window is closed, as expected.
+
+    - Specifying `--windowed` to PyInstaller will create an `.app` file on Mac which may be more convenient to distribute.
+    When you double-click the app to run it, it will not show any console output.
+    You can also run the app from the command line with `./myapp.app/Contents/MacOS/myapp` to see the console output.
+
+    - Specifying `--onefile` to PyInstaller will create a single executable file.
+    Whilst convenient for distribution, it will be slower to start up.
+    This is not NiceGUI's fault but just the way Pyinstaller zips things into a single file, then unzips everything
+    into a temporary directory before running.
+    You can mitigate this by removing `--onefile` from the PyInstaller command,
+    and zip up the generated `dist` directory yourself, distribute it,
+    and your end users can unzip once and be good to go,
+    without the constant expansion of files due to the `--onefile` flag.
+    
+    - Summary of user experience for different options:
+
+        | PyInstaller              | `ui.run(...)`  | Explanation |
+        | :---                     | :---           | :---        |
+        | `onefile`                | `native=False` | Single executable generated in `dist/`, runs in browser |
+        | `onefile`                | `native=True`  | Single executable generated in `dist/`, runs in popup window |
+        | `onefile` and `windowed` | `native=True`  | Single executable generated in `dist/` (on Mac a proper `dist/myapp.app` generated incl. icon), runs in popup window, no console appears |
+        | `onefile` and `windowed` | `native=False` | Avoid (no way to exit the app) |
+        | Specify neither          |                | A `dist/myapp` directory created which can be zipped manually and distributed; run with `dist/myapp/myapp` |
+
+    - If you are using a Python virtual environment, ensure you `pip install pyinstaller` within your virtual environment
+    so that the correct PyInstaller is used, or you may get broken apps due to the wrong version of PyInstaller being picked up.
+    That is why the build script invokes PyInstaller using `python -m PyInstaller` rather than just `pyinstaller`.
+''')
+
+
+@doc.ui
+def install_pyinstaller():
+    with bash_window(classes='max-w-lg w-full h-42 self-center'):
+        ui.markdown('''
+            ```bash
+            python -m venv venv
+            source venv/bin/activate
+            pip install nicegui
+            pip install pyinstaller
+            ```
+        ''')
+
+
+doc.text('', '''
+    **Note:**
+    If you're getting an error "TypeError: a bytes-like object is required, not 'str'", try adding the following lines to the top of your `main.py` file:
+    ```py
+    import sys
+    sys.stdout = open('logs.txt', 'w')
+    ```
+    See <https://github.com/zauberzeug/nicegui/issues/681> for more information.
+''')
+
+doc.text('', '''
+    **Common pitfalls on Mac M1**
+    
+    - If new processes are spawned in an endless loop, try adding the following lines at the beginning of your code:
+
+        ```python
+        from multiprocessing import freeze_support
+        freeze_support()
+        ```
+    
+    - If processes are left behind after closing the app, try packaging the app without the `--windowed` argument.
+''')
+
+doc.text('NiceGUI On Air', '''
+    By using `ui.run(on_air=True)` you can share your local app with others over the internet 🧞.
+
+    When accessing the on-air URL, all libraries (like Vue, Quasar, ...) are loaded from our CDN.
+    Thereby only the raw content and events need to be transmitted by your local app.
+    This makes it blazing fast even if your app only has a poor internet connection (e.g. a mobile robot in the field).
+
+    By setting `on_air=True` you will get a random URL which is valid for 1 hour.
+    If you sign-up at <https://on-air.nicegui.io> you get a token which could be used to identify your device: `ui.run(on_air='<your token>'`).
+    This will give you a fixed URL and the possibility to protect remote access with a passphrase.
+
+    Currently On Air is available as a tech preview and can be used free of charge (for now).
+    We will gradually improve stability, introduce payment options and extend the service with multi-device management, remote terminal access and more.
+    Please let us know your feedback on [GitHub](https://github.com/zauberzeug/nicegui/discussions),
+    [Reddit](https://www.reddit.com/r/nicegui/), or [Discord](https://discord.gg/TEpFeAaF4f).
+
+    **Data Privacy:**
+    We take your privacy very serious.
+    NiceGUI On Air does not log or store any content of the relayed data.
+''')

+ 26 - 0
website/documentation/content/section_controls.py

@@ -0,0 +1,26 @@
+from . import (badge_documentation, button_documentation, checkbox_documentation, color_input_documentation,
+               color_picker_documentation, date_documentation, doc, input_documentation, joystick_documentation,
+               knob_documentation, number_documentation, radio_documentation, select_documentation,
+               slider_documentation, switch_documentation, textarea_documentation, time_documentation,
+               toggle_documentation, upload_documentation)
+
+doc.title('*Controls*')
+
+doc.intro(button_documentation)
+doc.intro(badge_documentation)
+doc.intro(toggle_documentation)
+doc.intro(radio_documentation)
+doc.intro(select_documentation)
+doc.intro(checkbox_documentation)
+doc.intro(switch_documentation)
+doc.intro(slider_documentation)
+doc.intro(joystick_documentation)
+doc.intro(input_documentation)
+doc.intro(textarea_documentation)
+doc.intro(number_documentation)
+doc.intro(knob_documentation)
+doc.intro(color_input_documentation)
+doc.intro(color_picker_documentation)
+doc.intro(date_documentation)
+doc.intro(time_documentation)
+doc.intro(upload_documentation)

+ 28 - 0
website/documentation/content/section_data_elements.py

@@ -0,0 +1,28 @@
+from nicegui import optional_features
+
+from . import (aggrid_documentation, circular_progress_documentation, code_documentation, doc, echart_documentation,
+               editor_documentation, highchart_documentation, json_editor_documentation, line_plot_documentation,
+               linear_progress_documentation, log_documentation, plotly_documentation, pyplot_documentation,
+               scene_documentation, spinner_documentation, table_documentation, tree_documentation)
+
+doc.title('*Data* Elements')
+
+doc.intro(table_documentation)
+doc.intro(aggrid_documentation)
+if optional_features.has('highcharts'):
+    doc.intro(highchart_documentation)
+doc.intro(echart_documentation)
+if optional_features.has('matplotlib'):
+    doc.intro(pyplot_documentation)
+    doc.intro(line_plot_documentation)
+if optional_features.has('plotly'):
+    doc.intro(plotly_documentation)
+doc.intro(linear_progress_documentation)
+doc.intro(circular_progress_documentation)
+doc.intro(spinner_documentation)
+doc.intro(scene_documentation)
+doc.intro(tree_documentation)
+doc.intro(log_documentation)
+doc.intro(editor_documentation)
+doc.intro(code_documentation)
+doc.intro(json_editor_documentation)

+ 88 - 0
website/documentation/content/section_page_layout.py

@@ -0,0 +1,88 @@
+from nicegui import ui
+
+from . import (card_documentation, carousel_documentation, column_documentation, context_menu_documentation,
+               dialog_documentation, doc, expansion_documentation, grid_documentation, menu_documentation,
+               notify_documentation, pagination_documentation, row_documentation, scroll_area_documentation,
+               separator_documentation, splitter_documentation, stepper_documentation, tabs_documentation,
+               timeline_documentation)
+
+doc.title('Page *Layout*')
+
+
+@doc.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)
+
+
+doc.intro(card_documentation)
+doc.intro(column_documentation)
+doc.intro(row_documentation)
+doc.intro(grid_documentation)
+
+
+@doc.demo('Clear Containers', '''
+    To remove all elements from a row, column or card container, use can call
+    ```py
+    container.clear()
+    ```
+
+    Alternatively, you can remove individual elements by calling
+    
+    - `container.remove(element: Element)`,
+    - `container.remove(index: int)`, or
+    - `element.delete()`.
+''')
+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) if list(container) else None)
+    ui.button('Clear', on_click=container.clear)
+
+
+doc.intro(expansion_documentation)
+doc.intro(scroll_area_documentation)
+doc.intro(separator_documentation)
+doc.intro(splitter_documentation)
+doc.intro(tabs_documentation)
+doc.intro(stepper_documentation)
+doc.intro(timeline_documentation)
+doc.intro(carousel_documentation)
+doc.intro(pagination_documentation)
+doc.intro(menu_documentation)
+doc.intro(context_menu_documentation)
+
+
+@doc.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(icon='thumb_up'):
+        ui.tooltip('I like this').classes('bg-green')
+
+
+doc.intro(notify_documentation)
+doc.intro(dialog_documentation)

Неке датотеке нису приказане због велике количине промена