Kaynağa Gözat

Merge branch 'main' into notification

Falko Schindler 1 yıl önce
ebeveyn
işleme
764e04b78c
100 değiştirilmiş dosya ile 3336 ekleme ve 792 silme
  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
   given-names: Rodja
   orcid: https://orcid.org/0009-0009-4735-6227
   orcid: https://orcid.org/0009-0009-4735-6227
 title: 'NiceGUI: Web-based user interfaces with Python. The nice way.'
 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
 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`.
 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.
 6. Create a new file with your new element alongside the existing ones.
 7. Make sure your element works as expected.
 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.
    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).
 9. Create a pull-request (see below).
 
 
 ### Additional Demos
 ### Additional Demos

+ 1 - 1
fetch_milestone.py

@@ -37,7 +37,7 @@ notes: Dict[str, List[str]] = {
 for issue in issues:
 for issue in issues:
     title: str = issue['title']
     title: str = issue['title']
     user: str = issue['user']['login']
     user: str = issue['user']['login']
-    body: str = issue['body']
+    body: str = issue['body'] or ''
     labels: list[str] = [label['name'] for label in issue['labels']]
     labels: list[str] = [label['name'] for label in issue['labels']]
     number_patterns = [r'#(\d+)', r'https://github.com/zauberzeug/nicegui/(?:issues|discussions|pulls)/(\d+)']
     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)]
     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
 #!/usr/bin/env python3
-import importlib
-import inspect
-import logging
 import os
 import os
 from pathlib import Path
 from pathlib import Path
-from typing import Awaitable, Callable, Optional
-from urllib.parse import parse_qs
 
 
 from fastapi import Request
 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.middleware.sessions import SessionMiddleware
-from starlette.types import ASGIApp, Receive, Scope, Send
 
 
 import prometheus
 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)
 prometheus.start_monitor(app)
 
 
 # session middleware is required for demo in documentation and prometheus
 # session middleware is required for demo in documentation and prometheus
 app.add_middleware(SessionMiddleware, secret_key=os.environ.get('NICEGUI_SECRET_KEY', ''))
 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('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
 app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 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_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')
 @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.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('/')
 @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')
 @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}')
 @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')
 @app.get('/status')
-def status():
-    """for health checks"""
+def _status():
     return 'Ok'
     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 asyncio
 import gzip
 import gzip
+import json
 import re
 import re
 from typing import Any, Dict, Optional
 from typing import Any, Dict, Optional
 
 
@@ -55,7 +56,8 @@ class Air:
         @self.relay.on('ready')
         @self.relay.on('ready')
         def _handle_ready(data: Dict[str, Any]) -> None:
         def _handle_ready(data: Dict[str, Any]) -> None:
             core.app.urls.add(data['device_url'])
             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')
         @self.relay.on('error')
         def _handleerror(data: Dict[str, Any]) -> None:
         def _handleerror(data: Dict[str, Any]) -> None:
@@ -85,8 +87,10 @@ class Air:
             if client_id not in Client.instances:
             if client_id not in Client.instances:
                 return
                 return
             client = Client.instances[client_id]
             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'])
             client.handle_event(data['msg'])
 
 
         @self.relay.on('javascript_response')
         @self.relay.on('javascript_response')

+ 3 - 0
nicegui/app/app_config.py

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

+ 19 - 5
nicegui/client.py

@@ -12,9 +12,7 @@ from fastapi import Request
 from fastapi.responses import Response
 from fastapi.responses import Response
 from fastapi.templating import Jinja2Templates
 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 .awaitable_response import AwaitableResponse
 from .dependencies import generate_resources
 from .dependencies import generate_resources
 from .element import Element
 from .element import Element
@@ -38,6 +36,12 @@ class Client:
     auto_index_client: Client
     auto_index_client: Client
     """The client that is used to render the auto-index page."""
     """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:
     def __init__(self, page: page, *, shared: bool = False) -> None:
         self.id = str(uuid.uuid4())
         self.id = str(uuid.uuid4())
         self.created = time.time()
         self.created = time.time()
@@ -59,8 +63,8 @@ class Client:
 
 
         self.waiting_javascript_commands: Dict[str, Any] = {}
         self.waiting_javascript_commands: Dict[str, Any] = {}
 
 
-        self.head_html = ''
-        self.body_html = ''
+        self._head_html = ''
+        self._body_html = ''
 
 
         self.page = page
         self.page = page
 
 
@@ -84,6 +88,16 @@ class Client:
         """Return True if the client is connected, False otherwise."""
         """Return True if the client is connected, False otherwise."""
         return self.environ is not None
         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):
     def __enter__(self):
         self.content.__enter__()
         self.content.__enter__()
         return self
         return self

+ 1 - 1
nicegui/elements/chat_message.py

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

+ 18 - 3
nicegui/elements/highchart.py

@@ -1,10 +1,25 @@
 from .. import optional_features
 from .. import optional_features
+from ..element import Element
+from ..logging import log
+from .markdown import Markdown
 
 
 try:
 try:
     from nicegui_highcharts import highchart
     from nicegui_highcharts import highchart
     optional_features.register('highcharts')
     optional_features.register('highcharts')
     __all__ = ['highchart']
     __all__ = ['highchart']
 except ImportError:
 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 pathlib import Path
 from typing import Union
 from typing import Union
 
 
-from PIL.Image import Image as PIL_Image
-
+from .. import optional_features
 from .mixins.source_element import SourceElement
 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'):
 class Image(SourceElement, component='image.js'):
     PIL_CONVERT_FORMAT = 'PNG'
     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
         """Image
 
 
         Displays an image.
         Displays an image.
@@ -22,8 +27,8 @@ class Image(SourceElement, component='image.js'):
         """
         """
         super().__init__(source=source)
         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)
             source = pil_to_base64(source, self.PIL_CONVERT_FORMAT)
         super()._set_props(source)
         super()._set_props(source)
 
 
@@ -33,7 +38,7 @@ class Image(SourceElement, component='image.js'):
         self.update()
         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.
     """Convert a PIL image to a base64 string which can be used as image source.
 
 
     :param pil_image: the PIL image
     :param pil_image: the PIL image

+ 10 - 5
nicegui/elements/interactive_image.py

@@ -4,20 +4,25 @@ import time
 from pathlib import Path
 from pathlib import Path
 from typing import Any, Callable, List, Optional, Union, cast
 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 ..events import GenericEventArguments, MouseEventArguments, handle_event
 from .image import pil_to_base64
 from .image import pil_to_base64
 from .mixins.content_element import ContentElement
 from .mixins.content_element import ContentElement
 from .mixins.source_element import SourceElement
 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'):
 class InteractiveImage(SourceElement, ContentElement, component='interactive_image.js'):
     CONTENT_PROP = 'content'
     CONTENT_PROP = 'content'
     PIL_CONVERT_FORMAT = 'PNG'
     PIL_CONVERT_FORMAT = 'PNG'
 
 
     def __init__(self,
     def __init__(self,
-                 source: Union[str, Path] = '', *,
+                 source: Union[str, Path, 'PIL_Image'] = '', *,
                  content: str = '',
                  content: str = '',
                  on_mouse: Optional[Callable[..., Any]] = None,
                  on_mouse: Optional[Callable[..., Any]] = None,
                  events: List[str] = ['click'],
                  events: List[str] = ['click'],
@@ -61,8 +66,8 @@ class InteractiveImage(SourceElement, ContentElement, component='interactive_ima
             handle_event(on_mouse, arguments)
             handle_event(on_mouse, arguments)
         self.on('mouse', handle_mouse)
         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)
             source = pil_to_base64(source, self.PIL_CONVERT_FORMAT)
         super()._set_props(source)
         super()._set_props(source)
 
 

+ 31 - 6
nicegui/functions/html.py

@@ -1,11 +1,36 @@
 from .. import context
 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 .. import context
 from ..client import Client
 from ..client import Client
+from ..element import Element
 from ..logging import log
 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
     """Open
 
 
     Can be used to programmatically trigger redirects for a specific client.
     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.
     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.
     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)
     :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()
     client = context.get_client()
     if client.has_socket_connection:
     if client.has_socket_connection:
         client.open(path, new_tab)
         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)
         # webview depends on bottle which uses the deprecated CGI function (https://github.com/bottlepy/bottle/issues/1403)
         warnings.filterwarnings('ignore', category=DeprecationWarning)
         warnings.filterwarnings('ignore', category=DeprecationWarning)
         import webview
         import webview
-    optional_features.register('native')
+    optional_features.register('webview')
 except ModuleNotFoundError:
 except ModuleNotFoundError:
     pass
     pass
 
 
@@ -104,7 +104,7 @@ def activate(host: str, port: int, title: str, width: int, height: int, fullscre
             time.sleep(0.1)
             time.sleep(0.1)
         _thread.interrupt_main()
         _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'
         log.error('Native mode is not supported in this configuration.\n'
                   'Please run "pip install pywebview" to use it.')
                   'Please run "pip install pywebview" to use it.')
         sys.exit(1)
         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.staticfiles import StaticFiles
 from fastapi_socketio import SocketManager
 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 .app import App
 from .client import Client
 from .client import Client
 from .dependencies import js_components, libraries
 from .dependencies import js_components, libraries
@@ -26,7 +26,7 @@ from .version import __version__
 
 
 @asynccontextmanager
 @asynccontextmanager
 async def _lifespan(_: App):
 async def _lifespan(_: App):
-    _startup()
+    await _startup()
     yield
     yield
     await _shutdown()
     await _shutdown()
 
 
@@ -76,9 +76,8 @@ def _get_component(key: str) -> FileResponse:
     raise HTTPException(status_code=404, detail=f'component "{key}" not found')
     raise HTTPException(status_code=404, detail=f'component "{key}" not found')
 
 
 
 
-def _startup() -> None:
+async def _startup() -> None:
     """Handle the startup event."""
     """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:
     if not app.config.has_run_config:
         raise RuntimeError('\n\n'
         raise RuntimeError('\n\n'
                            'You must call ui.run() to start the server.\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'
                            'remove the guard or replace it with\n'
                            '   if __name__ in {"__main__", "__mp_main__"}:\n'
                            '   if __name__ in {"__main__", "__mp_main__"}:\n'
                            'to allow for multiprocessing.')
                            '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_interval = max(app.config.reconnect_timeout * 0.8, 4)
     sio.eio.ping_timeout = max(app.config.reconnect_timeout * 0.4, 2)
     sio.eio.ping_timeout = max(app.config.reconnect_timeout * 0.4, 2)
     if core.app.config.favicon:
     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()
 _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."""
     """Register an optional feature."""
     _optional_features.add(feature)
     _optional_features.add(feature)
 
 
 
 
-def has(feature: str) -> bool:
+def has(feature: FEATURE) -> bool:
     """Check if an optional feature is registered."""
     """Check if an optional feature is registered."""
     return feature in _optional_features
     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 .client import Client
 from .favicon import create_favicon_route
 from .favicon import create_favicon_route
 from .language import Language
 from .language import Language
+from .logging import log
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from .api_router import APIRouter
     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
         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())
         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:
         async def decorated(*dec_args, **dec_kwargs) -> Response:
             request = dec_kwargs['request']
             request = dec_kwargs['request']
             # NOTE cleaning up the keyword args so the signature is consistent with "func" again
             # NOTE cleaning up the keyword args so the signature is consistent with "func" again
@@ -105,7 +110,11 @@ class page:
                     if time.time() > deadline:
                     if time.time() > deadline:
                         raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
                         raise TimeoutError(f'Response not ready after {self.response_timeout} seconds')
                     await asyncio.sleep(0.1)
                     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
             if isinstance(result, Response):  # NOTE if setup returns a response, we don't need to render the page
                 return result
                 return result
             binding._refresh_step()  # pylint: disable=protected-access
             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 air, core, helpers
 from . import native as native_module
 from . import native as native_module
-from . import welcome
 from .client import Client
 from .client import Client
 from .language import Language
 from .language import Language
 from .logging import log
 from .logging import log
@@ -22,7 +21,7 @@ APP_IMPORT_STRING = 'nicegui:app'
 
 
 def run(*,
 def run(*,
         host: Optional[str] = None,
         host: Optional[str] = None,
-        port: int = 8080,
+        port: Optional[int] = None,
         title: str = 'NiceGUI',
         title: str = 'NiceGUI',
         viewport: str = 'width=device-width, initial-scale=1',
         viewport: str = 'width=device-width, initial-scale=1',
         favicon: Optional[Union[str, Path]] = None,
         favicon: Optional[Union[str, Path]] = None,
@@ -45,6 +44,7 @@ def run(*,
         prod_js: bool = True,
         prod_js: bool = True,
         endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
         endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
         storage_secret: Optional[str] = None,
         storage_secret: Optional[str] = None,
+        show_welcome_message: bool = True,
         **kwargs: Any,
         **kwargs: Any,
         ) -> None:
         ) -> None:
     """ui.run
     """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.
     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 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 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 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)
     :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 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 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 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`    
     :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
     """
     """
     core.app.config.add_run_config(
     core.app.config.add_run_config(
@@ -89,6 +90,7 @@ def run(*,
         reconnect_timeout=reconnect_timeout,
         reconnect_timeout=reconnect_timeout,
         tailwind=tailwind,
         tailwind=tailwind,
         prod_js=prod_js,
         prod_js=prod_js,
+        show_welcome_message=show_welcome_message,
     )
     )
     core.app.config.endpoint_documentation = endpoint_documentation
     core.app.config.endpoint_documentation = endpoint_documentation
 
 
@@ -103,8 +105,6 @@ def run(*,
     if on_air:
     if on_air:
         air.instance = air.Air('' if on_air is True else 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':
     if multiprocessing.current_process().name != 'MainProcess':
         return
         return
 
 
@@ -121,12 +121,14 @@ def run(*,
     if native:
     if native:
         show = False
         show = False
         host = host or '127.0.0.1'
         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)
         width, height = window_size or (800, 600)
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
     else:
     else:
+        port = port or 8080
         host = host or '0.0.0.0'
         host = host or '0.0.0.0'
     assert host is not None
     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.
     # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
     os.environ['NICEGUI_HOST'] = host
     os.environ['NICEGUI_HOST'] = host
@@ -138,6 +140,9 @@ def run(*,
     def split_args(args: str) -> List[str]:
     def split_args(args: str) -> List[str]:
         return [a.strip() for a in args.split(',')]
         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`.
     # NOTE: The following lines are basically a copy of `uvicorn.run`, but keep a reference to the `server`.
 
 
     config = CustomServerConfig(
     config = CustomServerConfig(

+ 3 - 2
nicegui/ui_run_with.py

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

+ 7 - 5
nicegui/welcome.py

@@ -13,15 +13,17 @@ def _get_all_ips() -> List[str]:
     return ips
     return ips
 
 
 
 
-async def print_message() -> None:
+async def collect_urls() -> None:
     """Print a welcome message with URLs to access the NiceGUI app."""
     """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 = set((await run.io_bound(_get_all_ips)) if host == '0.0.0.0' else [])
     ips.discard('127.0.0.1')
     ips.discard('127.0.0.1')
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     urls = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     core.app.urls.update(urls)
     core.app.urls.update(urls)
     if len(urls) >= 2:
     if len(urls) >= 2:
         urls[-1] = 'and ' + urls[-1]
         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 os
+import re
 import threading
 import threading
 import time
 import time
 from contextlib import contextmanager
 from contextlib import contextmanager
-from typing import List, Optional
+from typing import List, Optional, Union
 
 
 import pytest
 import pytest
 from selenium import webdriver
 from selenium import webdriver
@@ -194,14 +195,18 @@ class Screen:
         print(f'Storing screenshot to {filename}')
         print(f'Storing screenshot to {filename}')
         self.selenium.get_screenshot_as_file(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:
         try:
             assert self.caplog.records, 'Expected a log message'
             assert self.caplog.records, 'Expected a log message'
             record = self.caplog.records[0]
             record = self.caplog.records[0]
             print(record.levelname, record.message)
             print(record.levelname, record.message)
             assert record.levelname.strip() == level, f'Expected "{level}" but got "{record.levelname}"'
             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:
         finally:
             self.caplog.records.clear()
             self.caplog.records.clear()
 
 

+ 13 - 0
tests/test_page.py

@@ -1,4 +1,5 @@
 import asyncio
 import asyncio
+import re
 from uuid import uuid4
 from uuid import uuid4
 
 
 from fastapi.responses import PlainTextResponse
 from fastapi.responses import PlainTextResponse
@@ -286,6 +287,18 @@ def test_returning_custom_response_async(screen: Screen):
     screen.should_not_contain('normal NiceGUI page')
     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):
 def test_reconnecting_without_page_reload(screen: Screen):
     @ui.page('/', reconnect_timeout=3.0)
     @ui.page('/', reconnect_timeout=3.0)
     def page():
     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__ = [
 __all__ = [
+    'anti_scroll_hack',
     'documentation',
     'documentation',
-    'example_card',
+    'fly',
+    'main_page',
     'svg',
     '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 .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__ = [
 __all__ = [
     'bash_window',
     'bash_window',
     'browser_window',
     'browser_window',
-    'create_anchor_name',
-    'create_overview',
-    'create_section',
-    'more',
+    'build_search_index',
     'create_intro',
     'create_intro',
-    'element_demo',
-    'generate_class_doc',
+    'overview',  # ensure documentation tree is built
     'python_window',
     '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:
 def main_demo() -> None:
     from nicegui import app
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.badge)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.button('Click me!', on_click=lambda: badge.set_text(int(badge.text) + 1)):
     with ui.button('Click me!', on_click=lambda: badge.set_text(int(badge.text) + 1)):
         badge = ui.badge('0', color='red').props('floating')
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.carousel)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.carousel(animated=True, arrows=True, navigation=True).props('height=180px'):
     with ui.carousel(animated=True, arrows=True, navigation=True).props('height=180px'):
         with ui.carousel_slide().classes('p-0'):
         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]')
             ui.image('https://picsum.photos/id/31/270/180').classes('w-[270px]')
         with ui.carousel_slide().classes('p-0'):
         with ui.carousel_slide().classes('p-0'):
             ui.image('https://picsum.photos/id/32/270/180').classes('w-[270px]')
             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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.checkbox)
 def main_demo() -> None:
 def main_demo() -> None:
     checkbox = ui.checkbox('check me')
     checkbox = ui.checkbox('check me')
     ui.label('Check!').bind_visibility_from(checkbox, 'value')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.code)
 def main_demo() -> None:
 def main_demo() -> None:
     ui.code('''
     ui.code('''
         from nicegui import ui
         from nicegui import ui
@@ -9,3 +12,6 @@ def main_demo() -> None:
             
             
         ui.run()
         ui.run()
     ''').classes('w-full')
     ''').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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.color_input)
 def main_demo() -> None:
 def main_demo() -> None:
     label = ui.label('Change my color!')
     label = ui.label('Change my color!')
     ui.color_input(label='Color', value='#000000',
     ui.color_input(label='Color', value='#000000',
                    on_change=lambda e: label.style(f'color:{e.value}'))
                    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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.color_picker)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.button(icon='colorize') as button:
     with ui.button(icon='colorize') as button:
         ui.color_picker(on_pick=lambda e: button.style(f'background-color:{e.color}!important'))
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.colors)
 def main_demo() -> None:
 def main_demo() -> None:
     # ui.button('Default', on_click=lambda: ui.colors())
     # ui.button('Default', on_click=lambda: ui.colors())
     # ui.button('Gray', on_click=lambda: ui.colors(primary='#555'))
     # ui.button('Gray', on_click=lambda: ui.colors(primary='#555'))
     # END OF DEMO
     # 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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.context_menu)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.image('https://picsum.photos/id/377/640/360'):
     with ui.image('https://picsum.photos/id/377/640/360'):
         with ui.context_menu():
         with ui.context_menu():
@@ -8,3 +11,6 @@ def main_demo() -> None:
             ui.menu_item('Flip vertically')
             ui.menu_item('Flip vertically')
             ui.separator()
             ui.separator()
             ui.menu_item('Reset')
             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 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:
 def main_demo() -> None:
     # dark = ui.dark_mode()
     # dark = ui.dark_mode()
     # ui.label('Switch mode:')
     # ui.label('Switch mode:')
@@ -19,3 +21,6 @@ def main_demo() -> None:
         l.style('color: black'),
         l.style('color: black'),
         c.style(f'background-color: {WINDOW_BG_COLORS["browser"][0]}'),
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.editor)
 def main_demo() -> None:
 def main_demo() -> None:
     editor = ui.editor(placeholder='Type something here')
     editor = ui.editor(placeholder='Type something here')
     ui.markdown().bind_content_from(editor, 'value',
     ui.markdown().bind_content_from(editor, 'value',
                                     backward=lambda v: f'HTML code:\n```\n{v}\n```')
                                     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.grid)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.grid(columns=2):
     with ui.grid(columns=2):
         ui.label('Name:')
         ui.label('Name:')
@@ -11,3 +14,6 @@ def main_demo() -> None:
 
 
         ui.label('Height:')
         ui.label('Height:')
         ui.label('1.80m')
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.html)
 def main_demo() -> None:
 def main_demo() -> None:
     ui.html('This is <strong>HTML</strong>.')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.joystick)
 def main_demo() -> None:
 def main_demo() -> None:
     ui.joystick(color='blue', size=50,
     ui.joystick(color='blue', size=50,
                 on_move=lambda e: coordinates.set_text(f"{e.x:.3f}, {e.y:.3f}"),
                 on_move=lambda e: coordinates.set_text(f"{e.x:.3f}, {e.y:.3f}"),
                 on_end=lambda _: coordinates.set_text('0, 0'))
                 on_end=lambda _: coordinates.set_text('0, 0'))
     coordinates = ui.label('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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.json_editor)
 def main_demo() -> None:
 def main_demo() -> None:
     json = {
     json = {
         'array': [1, 2, 3],
         'array': [1, 2, 3],
@@ -18,3 +21,6 @@ def main_demo() -> None:
     ui.json_editor({'content': {'json': json}},
     ui.json_editor({'content': {'json': json}},
                    on_select=lambda e: ui.notify(f'Select: {e}'),
                    on_select=lambda e: ui.notify(f'Select: {e}'),
                    on_change=lambda e: ui.notify(f'Change: {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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.keyboard)
 def main_demo() -> None:
 def main_demo() -> None:
     from nicegui.events import KeyEventArguments
     from nicegui.events import KeyEventArguments
 
 
@@ -23,3 +26,6 @@ def main_demo() -> None:
     keyboard = ui.keyboard(on_key=handle_key)
     keyboard = ui.keyboard(on_key=handle_key)
     ui.label('Key events can be caught globally by using the keyboard element.')
     ui.label('Key events can be caught globally by using the keyboard element.')
     ui.checkbox('Track key events').bind_value_to(keyboard, 'active')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.knob)
 def main_demo() -> None:
 def main_demo() -> None:
     knob = ui.knob(0.3, show_value=True)
     knob = ui.knob(0.3, show_value=True)
 
 
     with ui.knob(color='orange', track_color='grey-2').bind_value(knob, 'value'):
     with ui.knob(color='orange', track_color='grey-2').bind_value(knob, 'value'):
         ui.icon('volume_up')
         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 nicegui import events, ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.line_plot)
 def main_demo() -> None:
 def main_demo() -> None:
     import math
     import math
     from datetime import datetime
     from datetime import datetime
@@ -27,3 +30,6 @@ def main_demo() -> None:
         if line_checkbox.value:
         if line_checkbox.value:
             ui.timer(10.0, turn_off, once=True)
             ui.timer(10.0, turn_off, once=True)
     line_checkbox.on('update:model-value', handle_change, args=[None])
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.linear_progress)
 def main_demo() -> None:
 def main_demo() -> None:
     slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
     slider = ui.slider(min=0, max=1, step=0.01, value=0.5)
     ui.linear_progress().bind_value_from(slider, 'value')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.menu)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.row().classes('w-full items-center'):
     with ui.row().classes('w-full items-center'):
         result = ui.label().classes('mr-auto')
         result = ui.label().classes('mr-auto')
@@ -12,3 +15,6 @@ def main_demo() -> None:
                              lambda: result.set_text('Selected item 3'), auto_close=False)
                              lambda: result.set_text('Selected item 3'), auto_close=False)
                 ui.separator()
                 ui.separator()
                 ui.menu_item('Close', on_click=menu.close)
                 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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.mermaid)
 def main_demo() -> None:
 def main_demo() -> None:
     ui.mermaid('''
     ui.mermaid('''
     graph LR;
     graph LR;
         A --> B;
         A --> B;
         A --> C;
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.pagination)
 def main_demo() -> None:
 def main_demo() -> None:
     p = ui.pagination(1, 5, direction_links=True)
     p = ui.pagination(1, 5, direction_links=True)
     ui.label().bind_text_from(p, 'value', lambda v: f'Page {v}')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.pyplot)
 def main_demo() -> None:
 def main_demo() -> None:
     import numpy as np
     import numpy as np
     from matplotlib import pyplot as plt
     from matplotlib import pyplot as plt
@@ -9,3 +12,6 @@ def main_demo() -> None:
         x = np.linspace(0.0, 5.0)
         x = np.linspace(0.0, 5.0)
         y = np.cos(2 * np.pi * x) * np.exp(-x)
         y = np.cos(2 * np.pi * x) * np.exp(-x)
         plt.plot(x, y, '-')
         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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.radio)
 def main_demo() -> None:
 def main_demo() -> None:
     radio1 = ui.radio([1, 2, 3], value=1).props('inline')
     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')
     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 nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.row)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.row():
     with ui.row():
         ui.label('label 1')
         ui.label('label 1')
         ui.label('label 2')
         ui.label('label 2')
         ui.label('label 3')
         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)

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor