Explorar o código

Merge pull request #2084 from zauberzeug/documentation

Refactor documentation, improve search index
Rodja Trappe hai 1 ano
pai
achega
062c1428fc
Modificáronse 100 ficheiros con 4173 adicións e 774 borrados
  1. 3 2
      CONTRIBUTING.md
  2. 18 428
      main.py
  3. 4 15
      website/__init__.py
  4. 13 0
      website/anti_scroll_hack.py
  5. 0 160
      website/build_search_index.py
  6. 8 10
      website/documentation/__init__.py
  7. 0 110
      website/documentation/content.py
  8. 7 0
      website/documentation/content/__init__.py
  9. 4 1
      website/documentation/content/add_static_files_documentation.py
  10. 198 0
      website/documentation/content/aggrid_documentation.py
  11. 25 0
      website/documentation/content/audio_documentation.py
  12. 20 0
      website/documentation/content/avatar_documentation.py
  13. 6 0
      website/documentation/content/badge_documentation.py
  14. 64 0
      website/documentation/content/button_documentation.py
  15. 53 0
      website/documentation/content/card_documentation.py
  16. 6 0
      website/documentation/content/carousel_documentation.py
  17. 46 0
      website/documentation/content/chat_message_documentation.py
  18. 6 0
      website/documentation/content/checkbox_documentation.py
  19. 26 0
      website/documentation/content/circular_progress_documentation.py
  20. 6 0
      website/documentation/content/code_documentation.py
  21. 6 0
      website/documentation/content/color_input_documentation.py
  22. 6 0
      website/documentation/content/color_picker_documentation.py
  23. 8 2
      website/documentation/content/colors_documentation.py
  24. 26 0
      website/documentation/content/column_documentation.py
  25. 6 0
      website/documentation/content/context_menu_documentation.py
  26. 6 1
      website/documentation/content/dark_mode_documentation.py
  27. 37 0
      website/documentation/content/date_documentation.py
  28. 51 0
      website/documentation/content/dialog_documentation.py
  29. 12 0
      website/documentation/content/doc/__init__.py
  30. 144 0
      website/documentation/content/doc/api.py
  31. 20 0
      website/documentation/content/doc/page.py
  32. 30 0
      website/documentation/content/doc/part.py
  33. 15 0
      website/documentation/content/download_documentation.py
  34. 50 0
      website/documentation/content/echart_documentation.py
  35. 6 0
      website/documentation/content/editor_documentation.py
  36. 73 0
      website/documentation/content/element_documentation.py
  37. 22 0
      website/documentation/content/expansion_documentation.py
  38. 124 0
      website/documentation/content/generic_events_documentation.py
  39. 6 0
      website/documentation/content/grid_documentation.py
  40. 76 0
      website/documentation/content/highchart_documentation.py
  41. 6 0
      website/documentation/content/html_documentation.py
  42. 30 0
      website/documentation/content/icon_documentation.py
  43. 65 0
      website/documentation/content/image_documentation.py
  44. 45 0
      website/documentation/content/input_documentation.py
  45. 41 0
      website/documentation/content/interactive_image_documentation.py
  46. 6 0
      website/documentation/content/joystick_documentation.py
  47. 6 0
      website/documentation/content/json_editor_documentation.py
  48. 6 0
      website/documentation/content/keyboard_documentation.py
  49. 6 0
      website/documentation/content/knob_documentation.py
  50. 29 0
      website/documentation/content/label_documentation.py
  51. 6 0
      website/documentation/content/line_plot_documentation.py
  52. 6 0
      website/documentation/content/linear_progress_documentation.py
  53. 59 0
      website/documentation/content/link_documentation.py
  54. 42 0
      website/documentation/content/log_documentation.py
  55. 58 0
      website/documentation/content/markdown_documentation.py
  56. 6 0
      website/documentation/content/menu_documentation.py
  57. 6 0
      website/documentation/content/mermaid_documentation.py
  58. 33 0
      website/documentation/content/notify_documentation.py
  59. 33 0
      website/documentation/content/number_documentation.py
  60. 9 0
      website/documentation/content/open_documentation.py
  61. 121 0
      website/documentation/content/overview.py
  62. 86 0
      website/documentation/content/page_documentation.py
  63. 6 0
      website/documentation/content/pagination_documentation.py
  64. 70 0
      website/documentation/content/plotly_documentation.py
  65. 6 0
      website/documentation/content/pyplot_documentation.py
  66. 38 0
      website/documentation/content/query_documentation.py
  67. 6 0
      website/documentation/content/radio_documentation.py
  68. 94 0
      website/documentation/content/refreshable_documentation.py
  69. 6 0
      website/documentation/content/row_documentation.py
  70. 68 0
      website/documentation/content/run_documentation.py
  71. 51 0
      website/documentation/content/run_javascript_documentation.py
  72. 102 0
      website/documentation/content/scene_documentation.py
  73. 48 0
      website/documentation/content/scroll_area_documentation.py
  74. 152 0
      website/documentation/content/section_action_events.py
  75. 47 0
      website/documentation/content/section_audiovisual_elements.py
  76. 70 0
      website/documentation/content/section_binding_properties.py
  77. 283 0
      website/documentation/content/section_configuration_deployment.py
  78. 26 0
      website/documentation/content/section_controls.py
  79. 28 0
      website/documentation/content/section_data_elements.py
  80. 88 0
      website/documentation/content/section_page_layout.py
  81. 136 0
      website/documentation/content/section_pages_routing.py
  82. 127 0
      website/documentation/content/section_styling_appearance.py
  83. 12 0
      website/documentation/content/section_text_elements.py
  84. 53 0
      website/documentation/content/select_documentation.py
  85. 6 0
      website/documentation/content/separator_documentation.py
  86. 51 0
      website/documentation/content/slider_documentation.py
  87. 6 0
      website/documentation/content/spinner_documentation.py
  88. 44 0
      website/documentation/content/splitter_documentation.py
  89. 6 0
      website/documentation/content/stepper_documentation.py
  90. 46 45
      website/documentation/content/storage_documentation.py
  91. 6 0
      website/documentation/content/switch_documentation.py
  92. 308 0
      website/documentation/content/table_documentation.py
  93. 63 0
      website/documentation/content/tabs_documentation.py
  94. 21 0
      website/documentation/content/textarea_documentation.py
  95. 6 0
      website/documentation/content/time_documentation.py
  96. 6 0
      website/documentation/content/timeline_documentation.py
  97. 35 0
      website/documentation/content/timer_documentation.py
  98. 6 0
      website/documentation/content/toggle_documentation.py
  99. 65 0
      website/documentation/content/tree_documentation.py
  100. 39 0
      website/documentation/content/upload_documentation.py

+ 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

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

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

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

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

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

@@ -0,0 +1,144 @@
+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 _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

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

@@ -0,0 +1,20 @@
+from dataclasses import dataclass, field
+from typing import 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)
+
+    @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))

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

@@ -0,0 +1,121 @@
+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.
+''')
+
+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.ui
+def create_tiles():
+    with ui.grid().classes('grid-cols-[1fr] md:grid-cols-[1fr_1fr] xl:grid-cols-[1fr_1fr_1fr]'):
+        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')
+
+
+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.
+''')

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

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

@@ -0,0 +1,68 @@
+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)

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

+ 136 - 0
website/documentation/content/section_pages_routing.py

@@ -0,0 +1,136 @@
+import uuid
+
+from nicegui import app, ui
+
+from . import doc, download_documentation, open_documentation, page_documentation
+
+CONSTANT_UUID = str(uuid.uuid4())
+
+doc.title('*Pages* & Routing')
+
+doc.intro(page_documentation)
+
+
+@doc.demo('Auto-index page', '''
+    Pages created with the `@ui.page` decorator are "private".
+    Their content is re-created for each client.
+    Thus, in the demo to the right, the displayed ID on the private page changes when the browser reloads the page.
+
+    UI elements that are not wrapped in a decorated page function are placed on an automatically generated index page at route "/".
+    This auto-index page is created once on startup and *shared* across all clients that might connect.
+    Thus, each connected client will see the *same* elements.
+    In the demo to the right, the displayed ID on the auto-index page remains constant when the browser reloads the page.
+''')
+def auto_index_page():
+    from uuid import uuid4
+
+    @ui.page('/private_page')
+    async def private_page():
+        ui.label(f'private page with ID {uuid4()}')
+
+    # ui.label(f'shared auto-index page with ID {uuid4()}')
+    # ui.link('private page', private_page)
+    # END OF DEMO
+    ui.label(f'shared auto-index page with ID {CONSTANT_UUID}')
+    ui.link('private page', private_page)
+
+
+@doc.demo('Page Layout', '''
+    With `ui.header`, `ui.footer`, `ui.left_drawer` and `ui.right_drawer` you can add additional layout elements to a page.
+    The `fixed` argument controls whether the element should scroll or stay fixed on the screen.
+    The `top_corner` and `bottom_corner` arguments indicate whether a drawer should expand to the top or bottom of the page.
+    See <https://quasar.dev/layout/header-and-footer> and <https://quasar.dev/layout/drawer> for more information about possible props.
+    With `ui.page_sticky` you can place an element "sticky" on the screen.
+    See <https://quasar.dev/layout/page-sticky> for more information.
+''')
+def page_layout_demo():
+    @ui.page('/page_layout')
+    def page_layout():
+        ui.label('CONTENT')
+        [ui.label(f'Line {i}') for i in range(100)]
+        with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
+            ui.label('HEADER')
+            ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')
+        with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4'):
+            ui.label('LEFT DRAWER')
+        with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as right_drawer:
+            ui.label('RIGHT DRAWER')
+        with ui.footer().style('background-color: #3874c8'):
+            ui.label('FOOTER')
+
+    ui.link('show page with fancy layout', page_layout)
+
+
+@doc.demo('Parameter injection', '''
+    Thanks to FastAPI, a page function accepts optional parameters to provide
+    [path parameters](https://fastapi.tiangolo.com/tutorial/path-params/), 
+    [query parameters](https://fastapi.tiangolo.com/tutorial/query-params/) or the whole incoming
+    [request](https://fastapi.tiangolo.com/advanced/using-request-directly/) for accessing
+    the body payload, headers, cookies and more.
+''')
+def parameter_demo():
+    @ui.page('/icon/{icon}')
+    def icons(icon: str, amount: int = 1):
+        ui.label(icon).classes('text-h3')
+        with ui.row():
+            [ui.icon(icon).classes('text-h3') for _ in range(amount)]
+    ui.link('Star', '/icon/star?amount=5')
+    ui.link('Home', '/icon/home')
+    ui.link('Water', '/icon/water_drop?amount=3')
+
+
+doc.intro(open_documentation)
+doc.intro(download_documentation)
+
+
+@doc.demo(app.add_static_files)
+def add_static_files_demo():
+    from nicegui import app
+
+    app.add_static_files('/examples', 'examples')
+    ui.label('Some NiceGUI Examples').classes('text-h5')
+    ui.link('AI interface', '/examples/ai_interface/main.py')
+    ui.link('Custom FastAPI app', '/examples/fastapi/main.py')
+    ui.link('Authentication', '/examples/authentication/main.py')
+
+
+@doc.demo(app.add_media_files)
+def add_media_files_demo():
+    from pathlib import Path
+
+    import requests
+
+    from nicegui import app
+
+    media = Path('media')
+    # media.mkdir(exist_ok=True)
+    # r = requests.get('https://cdn.coverr.co/videos/coverr-cloudy-sky-2765/1080p.mp4')
+    # (media  / 'clouds.mp4').write_bytes(r.content)
+    # app.add_media_files('/my_videos', media)
+    # ui.video('/my_videos/clouds.mp4')
+    # END OF DEMO
+    ui.video('https://cdn.coverr.co/videos/coverr-cloudy-sky-2765/1080p.mp4')
+
+
+@doc.demo('API Responses', '''
+    NiceGUI is based on [FastAPI](https://fastapi.tiangolo.com/).
+    This means you can use all of FastAPI's features.
+    For example, you can implement a RESTful API in addition to your graphical user interface.
+    You simply import the `app` object from `nicegui`.
+    Or you can run NiceGUI on top of your own FastAPI app by using `ui.run_with(app)` instead of starting a server automatically with `ui.run()`.
+
+    You can also return any other FastAPI response object inside a page function.
+    For example, you can return a `RedirectResponse` to redirect the user to another page if certain conditions are met.
+    This is used in our [authentication demo](https://github.com/zauberzeug/nicegui/tree/main/examples/authentication/main.py).
+''')
+def fastapi_demo():
+    import random
+
+    from nicegui import app
+
+    @app.get('/random/{max}')
+    def generate_random_number(max: int):
+        return {'min': 0, 'max': max, 'value': random.randint(0, max)}
+
+    max = ui.number('max', value=100)
+    ui.button('generate random number', on_click=lambda: ui.open(f'/random/{max.value:.0f}'))

+ 127 - 0
website/documentation/content/section_styling_appearance.py

@@ -0,0 +1,127 @@
+from nicegui import events, ui
+
+from ..windows import browser_window, python_window
+from . import colors_documentation, dark_mode_documentation, doc, query_documentation
+
+doc.title('Styling & Appearance')
+
+
+@doc.demo('Styling', '''
+    NiceGUI uses the [Quasar Framework](https://quasar.dev/) version 1.0 and hence has its full design power.
+    Each NiceGUI element provides a `props` method whose content is passed [to the Quasar component](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components):
+    Have a look at [the Quasar documentation](https://quasar.dev/vue-components/button#design) for all styling props.
+    Props with a leading `:` can contain JavaScript expressions that are evaluated on the client.
+    You can also apply [Tailwind CSS](https://tailwindcss.com/) utility classes with the `classes` method.
+
+    If you really need to apply CSS, you can use the `style` method. Here the delimiter is `;` instead of a blank space.
+
+    All three functions also provide `remove` and `replace` parameters in case the predefined look is not wanted in a particular styling.
+''')
+def design_demo():
+    ui.radio(['x', 'y', 'z'], value='x').props('inline color=green')
+    ui.button(icon='touch_app').props('outline round').classes('shadow-lg')
+    ui.label('Stylish!').style('color: #6E93D6; font-size: 200%; font-weight: 300')
+
+
+doc.text('Try styling NiceGUI elements!', '''
+    Try out how
+    [Tailwind CSS classes](https://tailwindcss.com/),
+    [Quasar props](https://justpy.io/quasar_tutorial/introduction/#props-of-quasar-components),
+    and CSS styles affect NiceGUI elements.
+''')
+
+
+@doc.ui
+def styling_demo():
+    with ui.row():
+        ui.label('Select an element from those available and start styling it!').classes('mx-auto my-auto')
+        select_element = ui.select({
+            ui.label: 'ui.label',
+            ui.checkbox: 'ui.checkbox',
+            ui.switch: 'ui.switch',
+            ui.input: 'ui.input',
+            ui.textarea: 'ui.textarea',
+            ui.button: 'ui.button',
+        }, value=ui.button, on_change=lambda: live_demo_ui.refresh()).props('dense')
+
+    @ui.refreshable
+    def live_demo_ui():
+        with ui.column().classes('w-full items-stretch gap-8 no-wrap min-[1500px]:flex-row'):
+            with python_window(classes='w-full max-w-[44rem]'):
+                with ui.column().classes('w-full gap-4'):
+                    ui.markdown(f'''
+                        ```py
+                        from nicegui import ui
+
+                        element = {select_element.options[select_element.value]}('element')
+                        ```
+                    ''').classes('mb-[-0.25em]')
+                    with ui.row().classes('items-center gap-0 w-full px-2'):
+                        def handle_classes(e: events.ValueChangeEventArguments):
+                            try:
+                                element.classes(replace=e.value)
+                            except ValueError:
+                                pass
+                        ui.markdown("`element.classes('`")
+                        ui.input(on_change=handle_classes).classes('mt-[-0.5em] text-mono grow').props('dense')
+                        ui.markdown("`')`")
+                    with ui.row().classes('items-center gap-0 w-full px-2'):
+                        def handle_props(e: events.ValueChangeEventArguments):
+                            element._props = {'label': 'Button', 'color': 'primary'}
+                            try:
+                                element.props(e.value)
+                            except ValueError:
+                                pass
+                            element.update()
+                        ui.markdown("`element.props('`")
+                        ui.input(on_change=handle_props).classes('mt-[-0.5em] text-mono grow').props('dense')
+                        ui.markdown("`')`")
+                    with ui.row().classes('items-center gap-0 w-full px-2'):
+                        def handle_style(e: events.ValueChangeEventArguments):
+                            try:
+                                element.style(replace=e.value)
+                            except ValueError:
+                                pass
+                        ui.markdown("`element.style('`")
+                        ui.input(on_change=handle_style).classes('mt-[-0.5em] text-mono grow').props('dense')
+                        ui.markdown("`')`")
+                    ui.markdown('''
+                        ```py
+                        ui.run()
+                        ```
+                    ''')
+            with browser_window(classes='w-full max-w-[44rem] min-[1500px]:max-w-[20rem] min-h-[10rem] browser-window'):
+                element: ui.element = select_element.value("element")
+    live_demo_ui()
+
+
+@doc.demo('Tailwind CSS', '''
+    [Tailwind CSS](https://tailwindcss.com/) is a CSS framework for rapidly building custom user interfaces.
+    NiceGUI provides a fluent, auto-complete friendly interface for adding Tailwind classes to UI elements.
+    
+    You can discover available classes by navigating the methods of the `tailwind` property.
+    The builder pattern allows you to chain multiple classes together (as shown with "Label A").
+    You can also call the `tailwind` property with a list of classes (as shown with "Label B").
+
+    Although this is very similar to using the `classes` method, it is more convenient for Tailwind classes due to auto-completion.
+
+    Last but not least, you can also predefine a style and apply it to multiple elements (labels C and D).
+        
+    Note that sometimes Tailwind is overruled by Quasar styles, e.g. when using `ui.button('Button').tailwind('bg-red-500')`.
+    This is a known limitation and not fully in our control.
+    But we try to provide solutions like the `color` parameter: `ui.button('Button', color='red-500')`.
+''')
+def tailwind_demo():
+    from nicegui import Tailwind
+    ui.label('Label A').tailwind.font_weight('extrabold').text_color('blue-600').background_color('orange-200')
+    ui.label('Label B').tailwind('drop-shadow', 'font-bold', 'text-green-600')
+
+    red_style = Tailwind().text_color('red-600').font_weight('bold')
+    label_c = ui.label('Label C')
+    red_style.apply(label_c)
+    ui.label('Label D').tailwind(red_style)
+
+
+doc.intro(query_documentation)
+doc.intro(colors_documentation)
+doc.intro(dark_mode_documentation)

+ 12 - 0
website/documentation/content/section_text_elements.py

@@ -0,0 +1,12 @@
+from . import (chat_message_documentation, doc, element_documentation, html_documentation, label_documentation,
+               link_documentation, markdown_documentation, mermaid_documentation)
+
+doc.title('*Text* Elements')
+
+doc.intro(label_documentation)
+doc.intro(link_documentation)
+doc.intro(chat_message_documentation)
+doc.intro(element_documentation)
+doc.intro(markdown_documentation)
+doc.intro(mermaid_documentation)
+doc.intro(html_documentation)

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

@@ -0,0 +1,53 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.select)
+def main_demo() -> None:
+    select1 = ui.select([1, 2, 3], value=1)
+    select2 = ui.select({1: 'One', 2: 'Two', 3: 'Three'}).bind_value(select1, 'value')
+
+
+@doc.demo('Search-as-you-type', '''
+    You can activate `with_input` to get a text input with autocompletion.
+    The options will be filtered as you type.
+''')
+def search_as_you_type():
+    continents = [
+        'Asia',
+        'Africa',
+        'Antarctica',
+        'Europe',
+        'Oceania',
+        'North America',
+        'South America',
+    ]
+    ui.select(options=continents, with_input=True,
+              on_change=lambda e: ui.notify(e.value)).classes('w-40')
+
+
+@doc.demo('Multi selection', '''
+    You can activate `multiple` to allow the selection of more than one item.
+''')
+def multi_select():
+    names = ['Alice', 'Bob', 'Carol']
+    ui.select(names, multiple=True, value=names[:2], label='comma-separated') \
+        .classes('w-64')
+    ui.select(names, multiple=True, value=names[:2], label='with chips') \
+        .classes('w-64').props('use-chips')
+
+
+@doc.demo('Update options', '''
+    Options can be changed with the `options` property.
+    But then you also need to call `update()` afterwards to let the change take effect.
+    `set_options` is a shortcut that does both and works well for lambdas.
+''')
+def update_selection():
+    select = ui.select([1, 2, 3], value=1)
+    with ui.row():
+        ui.button('4, 5, 6', on_click=lambda: select.set_options([4, 5, 6], value=4))
+        ui.button('1, 2, 3', on_click=lambda: select.set_options([1, 2, 3], value=1))
+
+
+doc.reference(ui.select)

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

@@ -1,7 +1,13 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.separator)
 def main_demo() -> None:
 def main_demo() -> None:
     ui.label('text above')
     ui.label('text above')
     ui.separator()
     ui.separator()
     ui.label('text below')
     ui.label('text below')
+
+
+doc.reference(ui.separator)

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

@@ -0,0 +1,51 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.slider)
+def main_demo() -> None:
+    slider = ui.slider(min=0, max=100, value=50)
+    ui.label().bind_text_from(slider, 'value')
+
+
+@doc.demo('Throttle events with leading and trailing options', '''
+    By default the value change event of a slider is throttled to 0.05 seconds.
+    This means that if you move the slider quickly, the value will only be updated every 0.05 seconds.
+
+    By default both "leading" and "trailing" events are activated.
+    This means that the very first event is triggered immediately, and the last event is triggered after the throttle time.
+
+    This demo shows how disabling either of these options changes the behavior.
+    To see the effect more clearly, the throttle time is set to 1 second.
+    The first slider shows the default behavior, the second one only sends leading events, and the third only sends trailing events.
+''')
+def throttle_events_with_leading_and_trailing_options():
+    ui.label('default')
+    ui.slider(min=0, max=10, step=0.1, value=5).props('label-always') \
+        .on('update:model-value', lambda e: ui.notify(e.args),
+            throttle=1.0)
+
+    ui.label('leading events only')
+    ui.slider(min=0, max=10, step=0.1, value=5).props('label-always') \
+        .on('update:model-value', lambda e: ui.notify(e.args),
+            throttle=1.0, trailing_events=False)
+
+    ui.label('trailing events only')
+    ui.slider(min=0, max=10, step=0.1, value=5).props('label-always') \
+        .on('update:model-value', lambda e: ui.notify(e.args),
+            throttle=1.0, leading_events=False)
+
+
+@doc.demo('Disable slider', '''
+    You can disable a slider with the `disable()` method.
+    This will prevent the user from moving the slider.
+    The slider will also be grayed out.
+''')
+def disable_slider():
+    slider = ui.slider(min=0, max=100, value=50)
+    ui.button('Disable slider', on_click=slider.disable)
+    ui.button('Enable slider', on_click=slider.enable)
+
+
+doc.reference(ui.slider)

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

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

+ 44 - 0
website/documentation/content/splitter_documentation.py

@@ -0,0 +1,44 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.splitter)
+def main_demo() -> None:
+    with ui.splitter() as splitter:
+        with splitter.before:
+            ui.label('This is some content on the left hand side.').classes('mr-2')
+        with splitter.after:
+            ui.label('This is some content on the right hand side.').classes('ml-2')
+
+
+@doc.demo('Advanced usage', '''
+    This demo shows all the slots and parameters including a tooltip, a custom separator, and a callback.
+''')
+def advanced_usage() -> None:
+    with ui.splitter(horizontal=False, reverse=False, value=60,
+                     on_change=lambda e: ui.notify(e.value)) as splitter:
+        ui.tooltip('This is the default slot.').classes('bg-green')
+        with splitter.before:
+            ui.label('This is the left hand side.').classes('mr-2')
+        with splitter.after:
+            ui.label('This is the right hand side.').classes('ml-2')
+        with splitter.separator:
+            ui.icon('lightbulb').classes('text-green')
+
+    ui.number('Split value', format='%.1f').bind_value(splitter)
+
+
+@doc.demo('Image fun', '''
+    This demo shows how to use the splitter to display images side by side.
+''')
+def image_fun() -> None:
+    with ui.splitter().classes('w-72 h-48') \
+            .props('before-class=overflow-hidden after-class=overflow-hidden') as splitter:
+        with splitter.before:
+            ui.image('https://cdn.quasar.dev/img/parallax1.jpg').classes('w-72 absolute-top-left')
+        with splitter.after:
+            ui.image('https://cdn.quasar.dev/img/parallax1-bw.jpg').classes('w-72 absolute-top-right')
+
+
+doc.reference(ui.splitter)

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

@@ -1,6 +1,9 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.stepper)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.stepper().props('vertical').classes('w-full') as stepper:
     with ui.stepper().props('vertical').classes('w-full') as stepper:
         with ui.step('Preheat'):
         with ui.step('Preheat'):
@@ -17,3 +20,6 @@ def main_demo() -> None:
             with ui.stepper_navigation():
             with ui.stepper_navigation():
                 ui.button('Done', on_click=lambda: ui.notify('Yay!', type='positive'))
                 ui.button('Done', on_click=lambda: ui.notify('Yay!', type='positive'))
                 ui.button('Back', on_click=stepper.previous).props('flat')
                 ui.button('Back', on_click=stepper.previous).props('flat')
+
+
+doc.reference(ui.stepper)

+ 46 - 45
website/documentation/more/storage_documentation.py → website/documentation/content/storage_documentation.py

@@ -3,12 +3,16 @@ from datetime import datetime
 
 
 from nicegui import ui
 from nicegui import ui
 
 
-from ..tools import text_demo
+from . import doc
 
 
+counter = Counter()  # type: ignore
+start = datetime.now().strftime(r'%H:%M, %d %B %Y')
 
 
-def main_demo() -> None:
-    """Storage
 
 
+doc.title('Storage')
+
+
+@doc.demo('Storage', '''
     NiceGUI offers a straightforward method for data persistence within your application. 
     NiceGUI offers a straightforward method for data persistence within your application. 
     It features three built-in storage types:
     It features three built-in storage types:
 
 
@@ -26,7 +30,8 @@ def main_demo() -> None:
     The user storage and browser storage are only available within `page builder functions </documentation/page>`_
     The user storage and browser storage are only available within `page builder functions </documentation/page>`_
     because they are accessing the underlying `Request` object from FastAPI.
     because they are accessing the underlying `Request` object from FastAPI.
     Additionally these two types require the `storage_secret` parameter in`ui.run()` to encrypt the browser session cookie.
     Additionally these two types require the `storage_secret` parameter in`ui.run()` to encrypt the browser session cookie.
-    """
+''')
+def storage_demo():
     from nicegui import app
     from nicegui import app
 
 
     # @ui.page('/')
     # @ui.page('/')
@@ -44,44 +49,40 @@ def main_demo() -> None:
         ui.label().bind_text_from(app.storage.user, 'count')
         ui.label().bind_text_from(app.storage.user, 'count')
 
 
 
 
-counter = Counter()
-start = datetime.now().strftime('%H:%M, %d %B %Y')
-
-
-def more() -> None:
-    @text_demo('Counting page visits', '''
-        Here we are using the automatically available browser-stored session ID to count the number of unique page visits.
-    ''')
-    def page_visits():
-        from collections import Counter
-        from datetime import datetime
-
-        from nicegui import app
-
-        # counter = Counter()
-        # start = datetime.now().strftime('%H:%M, %d %B %Y')
-        #
-        # @ui.page('/')
-        # def index():
-        #     counter[app.storage.browser['id']] += 1
-        #     ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
-        #
-        # ui.run(storage_secret='private key to secure the browser session cookie')
-        # END OF DEMO
-        counter[app.storage.browser['id']] += 1
-        ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
-
-    @text_demo('Storing UI state', '''
-        Storage can also be used in combination with [`bindings`](/documentation/bindings).
-        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')
+@doc.demo('Counting page visits', '''
+    Here we are using the automatically available browser-stored session ID to count the number of unique page visits.
+''')
+def page_visits():
+    from collections import Counter
+    from datetime import datetime
+
+    from nicegui import app
+
+    # counter = Counter()
+    # start = datetime.now().strftime('%H:%M, %d %B %Y')
+    #
+    # @ui.page('/')
+    # def index():
+    #     counter[app.storage.browser['id']] += 1
+    #     ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
+    #
+    # ui.run(storage_secret='private key to secure the browser session cookie')
+    # END OF DEMO
+    counter[app.storage.browser['id']] += 1
+    ui.label(f'{len(counter)} unique views ({sum(counter.values())} overall) since {start}')
+
+
+@doc.demo('Storing UI state', '''
+    Storage can also be used in combination with [`bindings`](/documentation/bindings).
+    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')

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

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

+ 308 - 0
website/documentation/content/table_documentation.py

@@ -0,0 +1,308 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.table)
+def main_demo() -> None:
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
+        {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carol'},
+    ]
+    ui.table(columns=columns, rows=rows, row_key='name')
+
+
+@doc.demo('Table with expandable rows', '''
+    Scoped slots can be used to insert buttons that toggle the expand state of a table row.
+    See the [Quasar documentation](https://quasar.dev/vue-components/table#expanding-rows) for more information.
+''')
+def table_with_expandable_rows():
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name'},
+        {'name': 'age', 'label': 'Age', 'field': 'age'},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carol'},
+    ]
+
+    table = ui.table(columns=columns, rows=rows, row_key='name').classes('w-72')
+    table.add_slot('header', r'''
+        <q-tr :props="props">
+            <q-th auto-width />
+            <q-th v-for="col in props.cols" :key="col.name" :props="props">
+                {{ col.label }}
+            </q-th>
+        </q-tr>
+    ''')
+    table.add_slot('body', r'''
+        <q-tr :props="props">
+            <q-td auto-width>
+                <q-btn size="sm" color="accent" round dense
+                    @click="props.expand = !props.expand"
+                    :icon="props.expand ? 'remove' : 'add'" />
+            </q-td>
+            <q-td v-for="col in props.cols" :key="col.name" :props="props">
+                {{ col.value }}
+            </q-td>
+        </q-tr>
+        <q-tr v-show="props.expand" :props="props">
+            <q-td colspan="100%">
+                <div class="text-left">This is {{ props.row.name }}.</div>
+            </q-td>
+        </q-tr>
+    ''')
+
+
+@doc.demo('Show and hide columns', '''
+    Here is an example of how to show and hide columns in a table.
+''')
+def show_and_hide_columns():
+    from typing import Dict
+
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
+        {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carol'},
+    ]
+    table = ui.table(columns=columns, rows=rows, row_key='name')
+
+    def toggle(column: Dict, visible: bool) -> None:
+        column['classes'] = '' if visible else 'hidden'
+        column['headerClasses'] = '' if visible else 'hidden'
+        table.update()
+
+    with ui.button(icon='menu'):
+        with ui.menu(), ui.column().classes('gap-0 p-2'):
+            for column in columns:
+                ui.switch(column['label'], value=True, on_change=lambda e,
+                          column=column: toggle(column, e.value))
+
+
+@doc.demo('Table with drop down selection', '''
+    Here is an example of how to use a drop down selection in a table.
+    After emitting a `rename` event from the scoped slot, the `rename` function updates the table rows.
+''')
+def table_with_drop_down_selection():
+    from nicegui import events
+
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name'},
+        {'name': 'age', 'label': 'Age', 'field': 'age'},
+    ]
+    rows = [
+        {'id': 0, 'name': 'Alice', 'age': 18},
+        {'id': 1, 'name': 'Bob', 'age': 21},
+        {'id': 2, 'name': 'Carol'},
+    ]
+    name_options = ['Alice', 'Bob', 'Carol']
+
+    def rename(e: events.GenericEventArguments) -> None:
+        for row in rows:
+            if row['id'] == e.args['id']:
+                row['name'] = e.args['name']
+        ui.notify(f'Table.rows is now: {table.rows}')
+
+    table = ui.table(columns=columns, rows=rows, row_key='name').classes('w-full')
+    table.add_slot('body', r'''
+        <q-tr :props="props">
+            <q-td key="name" :props="props">
+                <q-select
+                    v-model="props.row.name"
+                    :options="''' + str(name_options) + r'''"
+                    @update:model-value="() => $parent.$emit('rename', props.row)"
+                />
+            </q-td>
+            <q-td key="age" :props="props">
+                {{ props.row.age }}
+            </q-td>
+        </q-tr>
+    ''')
+    table.on('rename', rename)
+
+
+@doc.demo('Table from Pandas DataFrame', '''
+    You can create a table from a Pandas DataFrame using the `from_pandas` method. 
+    This method takes a Pandas DataFrame as input and returns a table.
+''')
+def table_from_pandas_demo():
+    import pandas as pd
+
+    df = pd.DataFrame(data={'col1': [1, 2], 'col2': [3, 4]})
+    ui.table.from_pandas(df).classes('max-h-40')
+
+
+@doc.demo('Adding rows', '''
+    It's simple to add new rows with the `add_rows(dict)` method.
+''')
+def adding_rows():
+    import os
+    import random
+
+    def add():
+        item = os.urandom(10 // 2).hex()
+        table.add_rows({'id': item, 'count': random.randint(0, 100)})
+
+    ui.button('add', on_click=add)
+    columns = [
+        {'name': 'id', 'label': 'ID', 'field': 'id'},
+        {'name': 'count', 'label': 'Count', 'field': 'count'},
+    ]
+    table = ui.table(columns=columns, rows=[], row_key='id').classes('w-full')
+
+
+@doc.demo('Custom sorting and formatting', '''
+    You can define dynamic column attributes using a `:` prefix.
+    This way you can define custom sorting and formatting functions.
+
+    The following example allows sorting the `name` column by length.
+    The `age` column is formatted to show the age in years.
+''')
+def custom_formatting():
+    columns = [
+        {
+            'name': 'name',
+            'label': 'Name',
+            'field': 'name',
+            'sortable': True,
+            ':sort': '(a, b, rowA, rowB) => b.length - a.length',
+        },
+        {
+            'name': 'age',
+            'label': 'Age',
+            'field': 'age',
+            ':format': 'value => value + " years"',
+        },
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carl', 'age': 42},
+    ]
+    ui.table(columns=columns, rows=rows, row_key='name')
+
+
+@doc.demo('Toggle fullscreen', '''
+    You can toggle the fullscreen mode of a table using the `toggle_fullscreen()` method.
+''')
+def toggle_fullscreen():
+    table = ui.table(
+        columns=[{'name': 'name', 'label': 'Name', 'field': 'name'}],
+        rows=[{'name': 'Alice'}, {'name': 'Bob'}, {'name': 'Carol'}],
+    ).classes('w-full')
+
+    with table.add_slot('top-left'):
+        def toggle() -> None:
+            table.toggle_fullscreen()
+            button.props('icon=fullscreen_exit' if table.is_fullscreen else 'icon=fullscreen')
+        button = ui.button('Toggle fullscreen', icon='fullscreen', on_click=toggle).props('flat')
+
+
+@doc.demo('Pagination', '''
+    You can provide either a single integer or a dictionary to define pagination.
+
+    The dictionary can contain the following keys:
+
+    - `rowsPerPage`: The number of rows per page.
+    - `sortBy`: The column name to sort by.
+    - `descending`: Whether to sort in descending order.
+    - `page`: The current page (1-based).
+''')
+def pagination() -> None:
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'required': True, 'align': 'left'},
+        {'name': 'age', 'label': 'Age', 'field': 'age', 'sortable': True},
+    ]
+    rows = [
+        {'name': 'Elsa', 'age': 18},
+        {'name': 'Oaken', 'age': 46},
+        {'name': 'Hans', 'age': 20},
+        {'name': 'Sven'},
+        {'name': 'Olaf', 'age': 4},
+        {'name': 'Anna', 'age': 17},
+    ]
+    ui.table(columns=columns, rows=rows, pagination=3)
+    ui.table(columns=columns, rows=rows, pagination={'rowsPerPage': 4, 'sortBy': 'age', 'page': 2})
+
+
+@doc.demo('Computed fields', '''
+    You can use functions to compute the value of a column.
+    The function receives the row as an argument.
+    See the [Quasar documentation](https://quasar.dev/vue-components/table#defining-the-columns) for more information.
+''')
+def computed_fields():
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'align': 'left'},
+        {'name': 'length', 'label': 'Length', ':field': 'row => row.name.length'},
+    ]
+    rows = [
+        {'name': 'Alice'},
+        {'name': 'Bob'},
+        {'name': 'Christopher'},
+    ]
+    ui.table(columns=columns, rows=rows, row_key='name')
+
+
+@doc.demo('Conditional formatting', '''
+    You can use scoped slots to conditionally format the content of a cell.
+    See the [Quasar documentation](https://quasar.dev/vue-components/table#example--body-cell-slot)
+    for more information about body-cell slots.
+    
+    In this demo we use a `q-badge` to display the age in red if the person is under 21 years old.
+    We use the `body-cell-age` slot to insert the `q-badge` into the `age` column.
+    The ":color" attribute of the `q-badge` is set to "red" if the age is under 21, otherwise it is set to "green".
+    The colon in front of the "color" attribute indicates that the value is a JavaScript expression.
+''')
+def conditional_formatting():
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name'},
+        {'name': 'age', 'label': 'Age', 'field': 'age'},
+    ]
+    rows = [
+        {'name': 'Alice', 'age': 18},
+        {'name': 'Bob', 'age': 21},
+        {'name': 'Carol', 'age': 42},
+    ]
+    table = ui.table(columns=columns, rows=rows, row_key='name')
+    table.add_slot('body-cell-age', '''
+        <q-td key="age" :props="props">
+            <q-badge :color="props.value < 21 ? 'red' : 'green'">
+                {{ props.value }}
+            </q-badge>
+        </q-td>
+    ''')
+
+
+@doc.demo('Table cells with links', '''
+    Here is a demo of how to insert links into table cells.
+    We use the `body-cell-link` slot to insert an `<a>` tag into the `link` column.
+''')
+def table_cells_with_links():
+    columns = [
+        {'name': 'name', 'label': 'Name', 'field': 'name', 'align': 'left'},
+        {'name': 'link', 'label': 'Link', 'field': 'link', 'align': 'left'},
+    ]
+    rows = [
+        {'name': 'Google', 'link': 'https://google.com'},
+        {'name': 'Facebook', 'link': 'https://facebook.com'},
+        {'name': 'Twitter', 'link': 'https://twitter.com'},
+    ]
+    table = ui.table(columns=columns, rows=rows, row_key='name')
+    table.add_slot('body-cell-link', '''
+        <q-td :props="props">
+            <a :href="props.value">{{ props.value }}</a>
+        </q-td>
+    ''')
+
+
+doc.reference(ui.table)

+ 63 - 0
website/documentation/content/tabs_documentation.py

@@ -0,0 +1,63 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo('Tabs', '''
+    The elements `ui.tabs`, `ui.tab`, `ui.tab_panels`, and `ui.tab_panel` resemble
+    [Quasar's tabs](https://quasar.dev/vue-components/tabs) and
+    [tab panels](https://quasar.dev/vue-components/tab-panels>) API.
+
+    `ui.tabs` creates a container for the tabs. This could be placed in a `ui.header` for example.
+    `ui.tab_panels` creates a container for the tab panels with the actual content.
+    Each `ui.tab_panel` is associated with a `ui.tab` element.
+''')
+def main_demo() -> None:
+    with ui.tabs().classes('w-full') as tabs:
+        one = ui.tab('One')
+        two = ui.tab('Two')
+    with ui.tab_panels(tabs, value=two).classes('w-full'):
+        with ui.tab_panel(one):
+            ui.label('First tab')
+        with ui.tab_panel(two):
+            ui.label('Second tab')
+
+
+@doc.demo('Name, label, icon', '''
+    The `ui.tab` element has a `label` property that can be used to display a different text than the `name`.
+    The `name` can also be used instead of the `ui.tab` objects to associate a `ui.tab` with a `ui.tab_panel`. 
+    Additionally each tab can have an `icon`.
+''')
+def name_and_label():
+    with ui.tabs() as tabs:
+        ui.tab('h', label='Home', icon='home')
+        ui.tab('a', label='About', icon='info')
+    with ui.tab_panels(tabs, value='h').classes('w-full'):
+        with ui.tab_panel('h'):
+            ui.label('Main Content')
+        with ui.tab_panel('a'):
+            ui.label('Infos')
+
+
+@doc.demo('Switch tabs programmatically', '''
+    The `ui.tabs` and `ui.tab_panels` elements are derived from ValueElement which has a `set_value` method.
+    That can be used to switch tabs programmatically.
+''')
+def switch_tabs():
+    content = {'Tab 1': 'Content 1', 'Tab 2': 'Content 2', 'Tab 3': 'Content 3'}
+    with ui.tabs() as tabs:
+        for title in content:
+            ui.tab(title)
+    with ui.tab_panels(tabs).classes('w-full') as panels:
+        for title, text in content.items():
+            with ui.tab_panel(title):
+                ui.label(text)
+
+    ui.button('GoTo 1', on_click=lambda: panels.set_value('Tab 1'))
+    ui.button('GoTo 2', on_click=lambda: tabs.set_value('Tab 2'))
+
+
+doc.reference(ui.tabs, title='Reference for ui.tabs')
+doc.reference(ui.tabs, title='Reference for ui.tab')
+doc.reference(ui.tabs, title='Reference for ui.tab_panels')
+doc.reference(ui.tabs, title='Reference for ui.tab_panel')

+ 21 - 0
website/documentation/content/textarea_documentation.py

@@ -0,0 +1,21 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.textarea)
+def main_demo() -> None:
+    ui.textarea(label='Text', placeholder='start typing',
+                on_change=lambda e: result.set_text('you typed: ' + e.value))
+    result = ui.label()
+
+
+@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.textarea(value='some text').props('clearable')
+    ui.label().bind_text_from(i, 'value')
+
+
+doc.reference(ui.textarea)

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

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

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

@@ -1,6 +1,9 @@
 from nicegui import ui
 from nicegui import ui
 
 
+from . import doc
 
 
+
+@doc.demo(ui.timeline)
 def main_demo() -> None:
 def main_demo() -> None:
     with ui.timeline(side='right'):
     with ui.timeline(side='right'):
         ui.timeline_entry('Rodja and Falko start working on NiceGUI.',
         ui.timeline_entry('Rodja and Falko start working on NiceGUI.',
@@ -14,3 +17,6 @@ def main_demo() -> None:
                           title='Release of 1.0',
                           title='Release of 1.0',
                           subtitle='December 15, 2022',
                           subtitle='December 15, 2022',
                           icon='rocket')
                           icon='rocket')
+
+
+doc.reference(ui.timeline)

+ 35 - 0
website/documentation/content/timer_documentation.py

@@ -0,0 +1,35 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.timer)
+def main_demo() -> None:
+    from datetime import datetime
+
+    label = ui.label()
+    ui.timer(1.0, lambda: label.set_text(f'{datetime.now():%X}'))
+
+
+@doc.demo('Activate, deactivate and cancel a timer', '''
+    You can activate and deactivate a timer using the `active` property.
+    You can cancel a timer using the `cancel` method.
+    After canceling a timer, it cannot be activated anymore.
+''')
+def activate_deactivate_demo():
+    slider = ui.slider(min=0, max=1, value=0.5)
+    timer = ui.timer(0.1, lambda: slider.set_value((slider.value + 0.01) % 1.0))
+    ui.switch('active').bind_value_to(timer, 'active')
+    ui.button('Cancel', on_click=timer.cancel)
+
+
+@doc.demo('Call a function after a delay', '''
+    You can call a function after a delay using a timer with the `once` parameter.
+''')
+def call_after_delay_demo():
+    def handle_click():
+        ui.timer(1.0, lambda: ui.notify('Hi!'), once=True)
+    ui.button('Notify after 1 second', on_click=handle_click)
+
+
+doc.reference(ui.timer)

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

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

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

@@ -0,0 +1,65 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.tree)
+def main_demo() -> None:
+    ui.tree([
+        {'id': 'numbers', 'children': [{'id': '1'}, {'id': '2'}]},
+        {'id': 'letters', 'children': [{'id': 'A'}, {'id': 'B'}]},
+    ], label_key='id', on_select=lambda e: ui.notify(e.value))
+
+
+@doc.demo('Tree with custom header and body', '''
+    Scoped slots can be used to insert custom content into the header and body of a tree node.
+    See the [Quasar documentation](https://quasar.dev/vue-components/tree#customize-content) for more information.
+''')
+def tree_with_custom_header_and_body():
+    tree = ui.tree([
+        {'id': 'numbers', 'description': 'Just some numbers', 'children': [
+            {'id': '1', 'description': 'The first number'},
+            {'id': '2', 'description': 'The second number'},
+        ]},
+        {'id': 'letters', 'description': 'Some latin letters', 'children': [
+            {'id': 'A', 'description': 'The first letter'},
+            {'id': 'B', 'description': 'The second letter'},
+        ]},
+    ], label_key='id', on_select=lambda e: ui.notify(e.value))
+
+    tree.add_slot('default-header', '''
+        <span :props="props">Node <strong>{{ props.node.id }}</strong></span>
+    ''')
+    tree.add_slot('default-body', '''
+        <span :props="props">Description: "{{ props.node.description }}"</span>
+    ''')
+
+
+@doc.demo('Expand and collapse programmatically', '''
+    The whole tree or individual nodes can be toggled programmatically using the `expand()` and `collapse()` methods.
+    This even works if a node is disabled (e.g. not clickable by the user).
+''')
+def expand_programmatically():
+    t = ui.tree([
+        {'id': 'A', 'children': [{'id': 'A1'}, {'id': 'A2'}], 'disabled': True},
+        {'id': 'B', 'children': [{'id': 'B1'}, {'id': 'B2'}]},
+    ], label_key='id').expand()
+
+    with ui.row():
+        ui.button('+ all', on_click=t.expand)
+        ui.button('- all', on_click=t.collapse)
+        ui.button('+ A', on_click=lambda: t.expand(['A']))
+        ui.button('- A', on_click=lambda: t.collapse(['A']))
+
+
+@doc.demo('Tree with checkboxes', '''
+    The tree can be used with checkboxes by setting the "tick-strategy" prop.
+''')
+def tree_with_checkboxes():
+    ui.tree([
+        {'id': 'A', 'children': [{'id': 'A1'}, {'id': 'A2'}]},
+        {'id': 'B', 'children': [{'id': 'B1'}, {'id': 'B2'}]},
+    ], label_key='id', tick_strategy='leaf', on_tick=lambda e: ui.notify(e.value))
+
+
+doc.reference(ui.tree)

+ 39 - 0
website/documentation/content/upload_documentation.py

@@ -0,0 +1,39 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.upload)
+def main_demo() -> None:
+    ui.upload(on_upload=lambda e: ui.notify(f'Uploaded {e.name}')).classes('max-w-full')
+
+
+@doc.demo('Upload restrictions', '''
+    In this demo, the upload is restricted to a maximum file size of 1 MB.
+    When a file is rejected, a notification is shown.
+''')
+def upload_restrictions() -> None:
+    ui.upload(on_upload=lambda e: ui.notify(f'Uploaded {e.name}'),
+              on_rejected=lambda: ui.notify('Rejected!'),
+              max_file_size=1_000_000).classes('max-w-full')
+
+
+@doc.demo('Show file content', '''
+    In this demo, the uploaded markdown file is shown in a dialog.
+''')
+def show_file_content() -> None:
+    from nicegui import events
+
+    with ui.dialog().props('full-width') as dialog:
+        with ui.card():
+            content = ui.markdown()
+
+    def handle_upload(e: events.UploadEventArguments):
+        text = e.content.read().decode('utf-8')
+        content.set_content(text)
+        dialog.open()
+
+    ui.upload(on_upload=handle_upload).props('accept=.md').classes('max-w-full')
+
+
+doc.reference(ui.upload)

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio