Pārlūkot izejas kodu

Merge branch 'zauberzeug:main' into main

frankvp 1 gadu atpakaļ
vecāks
revīzija
4e0ade8b98

+ 3 - 3
CITATION.cff

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

+ 2 - 1
nicegui/air.py

@@ -56,7 +56,8 @@ class Air:
         @self.relay.on('ready')
         @self.relay.on('ready')
         def _handle_ready(data: Dict[str, Any]) -> None:
         def _handle_ready(data: Dict[str, Any]) -> None:
             core.app.urls.add(data['device_url'])
             core.app.urls.add(data['device_url'])
-            print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
+            if core.app.config.show_welcome_message:
+                print(f'NiceGUI is on air at {data["device_url"]}', flush=True)
 
 
         @self.relay.on('error')
         @self.relay.on('error')
         def _handleerror(data: Dict[str, Any]) -> None:
         def _handleerror(data: Dict[str, Any]) -> None:

+ 3 - 0
nicegui/app/app_config.py

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

+ 19 - 5
nicegui/client.py

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

+ 1 - 1
nicegui/elements/chat_message.py

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

+ 18 - 3
nicegui/elements/highchart.py

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

+ 11 - 6
nicegui/elements/image.py

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

+ 10 - 5
nicegui/elements/interactive_image.py

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

+ 31 - 6
nicegui/functions/html.py

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

+ 2 - 2
nicegui/native/native_mode.py

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

+ 5 - 4
nicegui/nicegui.py

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

+ 12 - 3
nicegui/optional_features.py

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

+ 5 - 4
nicegui/ui_run.py

@@ -11,7 +11,6 @@ from uvicorn.supervisors import ChangeReload, Multiprocess
 
 
 from . import air, core, helpers
 from . import air, core, helpers
 from . import native as native_module
 from . import native as native_module
-from . import welcome
 from .client import Client
 from .client import Client
 from .language import Language
 from .language import Language
 from .logging import log
 from .logging import log
@@ -45,6 +44,7 @@ def run(*,
         prod_js: bool = True,
         prod_js: bool = True,
         endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
         endpoint_documentation: Literal['none', 'internal', 'page', 'all'] = 'none',
         storage_secret: Optional[str] = None,
         storage_secret: Optional[str] = None,
+        show_welcome_message: bool = True,
         **kwargs: Any,
         **kwargs: Any,
         ) -> None:
         ) -> None:
     """ui.run
     """ui.run
@@ -76,6 +76,7 @@ def run(*,
     :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
     :param prod_js: whether to use the production version of Vue and Quasar dependencies (default: `True`)
     :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
     :param endpoint_documentation: control what endpoints appear in the autogenerated OpenAPI docs (default: 'none', options: 'none', 'internal', 'page', 'all')
     :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
     :param storage_secret: secret key for browser-based storage (default: `None`, a value is required to enable ui.storage.individual and ui.storage.browser)
+    :param show_welcome_message: whether to show the welcome message (default: `True`)
     :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
     :param kwargs: additional keyword arguments are passed to `uvicorn.run`    
     """
     """
     core.app.config.add_run_config(
     core.app.config.add_run_config(
@@ -89,6 +90,7 @@ def run(*,
         reconnect_timeout=reconnect_timeout,
         reconnect_timeout=reconnect_timeout,
         tailwind=tailwind,
         tailwind=tailwind,
         prod_js=prod_js,
         prod_js=prod_js,
+        show_welcome_message=show_welcome_message,
     )
     )
     core.app.config.endpoint_documentation = endpoint_documentation
     core.app.config.endpoint_documentation = endpoint_documentation
 
 
@@ -103,8 +105,6 @@ def run(*,
     if on_air:
     if on_air:
         air.instance = air.Air('' if on_air is True else on_air)
         air.instance = air.Air('' if on_air is True else on_air)
 
 
-    core.app.on_startup(welcome.print_message)
-
     if multiprocessing.current_process().name != 'MainProcess':
     if multiprocessing.current_process().name != 'MainProcess':
         return
         return
 
 
@@ -125,9 +125,10 @@ def run(*,
         width, height = window_size or (800, 600)
         width, height = window_size or (800, 600)
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
         native_module.activate(host, port, title, width, height, fullscreen, frameless)
     else:
     else:
-        port = port or 8080     
+        port = port or 8080
         host = host or '0.0.0.0'
         host = host or '0.0.0.0'
     assert host is not None
     assert host is not None
+    assert port is not None
 
 
     # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
     # NOTE: We save host and port in environment variables so the subprocess started in reload mode can access them.
     os.environ['NICEGUI_HOST'] = host
     os.environ['NICEGUI_HOST'] = host

+ 3 - 2
nicegui/ui_run_with.py

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

+ 7 - 5
nicegui/welcome.py

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

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

@@ -66,3 +66,17 @@ def svg_favicon():
     '''
     '''
 
 
     # ui.run(favicon=smiley)
     # ui.run(favicon=smiley)
+
+
+@doc.demo('Custom welcome message', '''
+    You can mute the default welcome message on the command line setting the `show_welcome_message` to `False`.
+    Instead you can print your own welcome message with a custom startup handler.
+''')
+def custom_welcome_message():
+    from nicegui import app
+
+    ui.label('App with custom welcome message')
+    #
+    # app.on_startup(lambda: print('Visit your app on one of these URLs:', app.urls))
+    #
+    # ui.run(show_welcome_message=False)

+ 2 - 2
website/documentation/rendering.py

@@ -44,9 +44,9 @@ def render_page(documentation: DocumentationPage, *, with_menu: bool = True) ->
                     description = part.description.replace('param ', '')
                     description = part.description.replace('param ', '')
                     html = docutils.core.publish_parts(description, writer_name='html5_polyglot')['html_body']
                     html = docutils.core.publish_parts(description, writer_name='html5_polyglot')['html_body']
                     html = apply_tailwind(html)
                     html = apply_tailwind(html)
-                    ui.html(html)
+                    ui.html(html).classes('bold-links arrow-links')
                 else:
                 else:
-                    ui.markdown(part.description)
+                    ui.markdown(part.description).classes('bold-links arrow-links')
             if part.ui:
             if part.ui:
                 part.ui()
                 part.ui()
             if part.demo:
             if part.demo: