Explorar el Código

Merge branch 'main' into input-error

Itay Raveh hace 2 años
padre
commit
c05d8e997f
Se han modificado 100 ficheros con 825 adiciones y 265 borrados
  1. 1 1
      .github/workflows/test.yml
  2. 4 4
      CITATION.cff
  3. 1 1
      development.dockerfile
  4. 3 1
      examples/authentication/main.py
  5. 14 13
      examples/chat_app/main.py
  6. 1 1
      examples/chat_with_ai/requirements.txt
  7. 2 2
      examples/local_file_picker/local_file_picker.py
  8. 109 0
      examples/simpy/async_realtime_environment.py
  9. 49 0
      examples/simpy/main.py
  10. 2 0
      examples/simpy/requirements.txt
  11. 65 0
      examples/todo_list/main.py
  12. 17 0
      fetch_dependencies.py
  13. 6 4
      fetch_tailwind.py
  14. 1 1
      fly.dockerfile
  15. 39 31
      main.py
  16. 10 0
      nicegui/__init__.py
  17. 5 5
      nicegui/app.py
  18. 6 6
      nicegui/background_tasks.py
  19. 5 5
      nicegui/binding.py
  20. 7 6
      nicegui/client.py
  21. 14 10
      nicegui/element.py
  22. 0 1
      nicegui/elements/aggrid.py
  23. 3 2
      nicegui/elements/avatar.py
  24. 2 2
      nicegui/elements/button.py
  25. 12 5
      nicegui/elements/chart.js
  26. 0 1
      nicegui/elements/chart.py
  27. 2 2
      nicegui/elements/checkbox.py
  28. 5 1
      nicegui/elements/choice_element.py
  29. 8 4
      nicegui/elements/color_input.py
  30. 2 2
      nicegui/elements/color_picker.py
  31. 0 1
      nicegui/elements/colors.py
  32. 2 2
      nicegui/elements/date.py
  33. 1 1
      nicegui/elements/input.py
  34. 7 3
      nicegui/elements/interactive_image.py
  35. 3 3
      nicegui/elements/joystick.py
  36. 2 2
      nicegui/elements/keyboard.py
  37. 9 3
      nicegui/elements/line_plot.py
  38. 2 3
      nicegui/elements/link.py
  39. 1 2
      nicegui/elements/log.py
  40. 6 2
      nicegui/elements/menu.py
  41. 8 5
      nicegui/elements/mixins/content_element.py
  42. 8 5
      nicegui/elements/mixins/disableable_element.py
  43. 8 5
      nicegui/elements/mixins/filter_element.py
  44. 8 5
      nicegui/elements/mixins/source_element.py
  45. 8 5
      nicegui/elements/mixins/text_element.py
  46. 13 5
      nicegui/elements/mixins/value_element.py
  47. 9 7
      nicegui/elements/mixins/visibility.py
  48. 4 3
      nicegui/elements/number.py
  49. 2 1
      nicegui/elements/pyplot.py
  50. 5 1
      nicegui/elements/radio.py
  51. 2 3
      nicegui/elements/scene.py
  52. 3 2
      nicegui/elements/select.py
  53. 3 2
      nicegui/elements/slider.py
  54. 3 2
      nicegui/elements/splitter.py
  55. 2 2
      nicegui/elements/switch.py
  56. 2 2
      nicegui/elements/table.py
  57. 3 2
      nicegui/elements/tabs.py
  58. 5 4
      nicegui/elements/textarea.py
  59. 4 4
      nicegui/elements/time.py
  60. 5 1
      nicegui/elements/toggle.py
  61. 4 3
      nicegui/elements/tree.py
  62. 3 3
      nicegui/elements/upload.py
  63. 4 4
      nicegui/events.py
  64. 1 1
      nicegui/functions/notify.py
  65. 2 2
      nicegui/functions/open.py
  66. 39 32
      nicegui/functions/refreshable.py
  67. 10 4
      nicegui/functions/timer.py
  68. 9 7
      nicegui/globals.py
  69. 1 1
      nicegui/helpers.py
  70. 3 0
      nicegui/json/orjson_wrapper.py
  71. 66 0
      nicegui/language.py
  72. 7 3
      nicegui/nicegui.py
  73. 10 3
      nicegui/page.py
  74. 6 2
      nicegui/run.py
  75. 3 0
      nicegui/run_with.py
  76. 4 1
      nicegui/slot.py
  77. 5 0
      nicegui/static/quasar.ar-TN.umd.prod.js
  78. 5 0
      nicegui/static/quasar.ar.umd.prod.js
  79. 5 0
      nicegui/static/quasar.az-Latn.umd.prod.js
  80. 5 0
      nicegui/static/quasar.bg.umd.prod.js
  81. 5 0
      nicegui/static/quasar.bn.umd.prod.js
  82. 5 0
      nicegui/static/quasar.ca.umd.prod.js
  83. 5 0
      nicegui/static/quasar.cs.umd.prod.js
  84. 5 0
      nicegui/static/quasar.da.umd.prod.js
  85. 5 0
      nicegui/static/quasar.de.umd.prod.js
  86. 5 0
      nicegui/static/quasar.el.umd.prod.js
  87. 5 0
      nicegui/static/quasar.en-GB.umd.prod.js
  88. 5 0
      nicegui/static/quasar.en-US.umd.prod.js
  89. 5 0
      nicegui/static/quasar.eo.umd.prod.js
  90. 5 0
      nicegui/static/quasar.es.umd.prod.js
  91. 5 0
      nicegui/static/quasar.et.umd.prod.js
  92. 5 0
      nicegui/static/quasar.eu.umd.prod.js
  93. 5 0
      nicegui/static/quasar.fa-IR.umd.prod.js
  94. 5 0
      nicegui/static/quasar.fa.umd.prod.js
  95. 5 0
      nicegui/static/quasar.fi.umd.prod.js
  96. 5 0
      nicegui/static/quasar.fr.umd.prod.js
  97. 5 0
      nicegui/static/quasar.gn.umd.prod.js
  98. 5 0
      nicegui/static/quasar.he.umd.prod.js
  99. 5 0
      nicegui/static/quasar.hr.umd.prod.js
  100. 5 0
      nicegui/static/quasar.hu.umd.prod.js

+ 1 - 1
.github/workflows/test.yml

@@ -25,7 +25,7 @@ jobs:
           poetry config virtualenvs.create false
           poetry install
           # install packages to run the examples
-          pip install opencv-python opencv-contrib-python-headless httpx replicate langchain openai
+          pip install opencv-python opencv-contrib-python-headless httpx replicate langchain openai simpy
           # try fix issue with importlib_resources
           pip install importlib-resources
       - name: test startup

+ 4 - 4
CITATION.cff

@@ -7,8 +7,8 @@ authors:
 - family-names: Trappe
   given-names: Rodja
   orcid: https://orcid.org/0009-0009-4735-6227
-title: 'NiceGUI: Web-based interfaces with Python. The nice way.'
-version: v1.2.11
-date-released: '2023-05-02'
+title: 'NiceGUI: Web-based user interfaces with Python. The nice way.'
+version: v1.2.15
+date-released: '2023-05-27'
 url: https://github.com/zauberzeug/nicegui
-doi: 10.5281/zenodo.7887694
+doi: 10.5281/zenodo.7976420

+ 1 - 1
development.dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.7-slim
+FROM python:3.7.16-slim
 
 RUN apt update && apt install curl -y
 

+ 3 - 1
examples/authentication/main.py

@@ -7,6 +7,7 @@ use the great `Authlib package <https://docs.authlib.org/en/v0.13/client/starlet
 Here we just demonstrate the NiceGUI integration.
 '''
 
+import os
 import uuid
 from typing import Dict
 
@@ -16,7 +17,8 @@ from starlette.middleware.sessions import SessionMiddleware
 
 from nicegui import app, ui
 
-app.add_middleware(SessionMiddleware, secret_key='some_random_string')  # use your own secret key here
+# put your your own secret key in an environment variable MY_SECRET_KEY
+app.add_middleware(SessionMiddleware, secret_key=os.environ.get('MY_SECRET_KEY', ''))
 
 # in reality users and session_info would be persistent (e.g. database, file, ...) and passwords obviously hashed
 users = [('user1', 'pass1'), ('user2', 'pass2')]

+ 14 - 13
examples/chat_app/main.py

@@ -1,20 +1,17 @@
 #!/usr/bin/env python3
 from datetime import datetime
 from typing import List, Tuple
+from uuid import uuid4
 
 from nicegui import Client, ui
 
-messages: List[Tuple[str, str, str]] = []
+messages: List[Tuple[str, str, str, str]] = []
 
 
 @ui.refreshable
-async def chat_messages(name_input: ui.input) -> None:
-    for name, text, stamp in messages:
-        ui.chat_message(text=text,
-                        name=name,
-                        stamp=stamp,
-                        avatar=f'https://robohash.org/{name or "anonymous"}?bgset=bg2',
-                        sent=name == name_input.value)
+async def chat_messages(own_id: str) -> None:
+    for user_id, avatar, text, stamp in messages:
+        ui.chat_message(text=text, stamp=stamp, avatar=avatar, sent=own_id == user_id)
     await ui.run_javascript('window.scrollTo(0, document.body.scrollHeight)', respond=False)
 
 
@@ -22,22 +19,26 @@ async def chat_messages(name_input: ui.input) -> None:
 async def main(client: Client):
     def send() -> None:
         stamp = datetime.utcnow().strftime('%X')
-        messages.append((name.value, text.value, stamp))
+        messages.append((user_id, avatar, text.value, stamp))
         text.value = ''
         chat_messages.refresh()
 
+    user_id = str(uuid4())
+    avatar = f'https://robohash.org/{user_id}?bgset=bg2'
+
     anchor_style = r'a:link, a:visited {color: inherit !important; text-decoration: none; font-weight: 500}'
     ui.add_head_html(f'<style>{anchor_style}</style>')
     with ui.footer().classes('bg-white'), ui.column().classes('w-full max-w-3xl mx-auto my-6'):
         with ui.row().classes('w-full no-wrap items-center'):
-            name = ui.input(placeholder='name').props('rounded outlined autofocus input-class=mx-3')
-            text = ui.input(placeholder='message').props('rounded outlined input-class=mx-3') \
-                .classes('w-full self-center').on('keydown.enter', send)
+            with ui.avatar().on('click', lambda: ui.open(main)):
+                ui.image(avatar)
+            text = ui.input(placeholder='message').on('keydown.enter', send) \
+                .props('rounded outlined input-class=mx-3').classes('flex-grow')
         ui.markdown('simple chat app built with [NiceGUI](https://nicegui.io)') \
             .classes('text-xs self-end mr-8 m-[-1em] text-primary')
 
     await client.connected()  # chat_messages(...) uses run_javascript which is only possible after connecting
     with ui.column().classes('w-full max-w-2xl mx-auto items-stretch'):
-        await chat_messages(name_input=name)
+        await chat_messages(user_id)
 
 ui.run()

+ 1 - 1
examples/chat_with_ai/requirements.txt

@@ -1,3 +1,3 @@
-langchain
+langchain>=0.0.142
 nicegui
 openai

+ 2 - 2
examples/local_file_picker/local_file_picker.py

@@ -1,5 +1,5 @@
 from pathlib import Path
-from typing import Optional
+from typing import Dict, Optional
 
 from nicegui import ui
 
@@ -58,7 +58,7 @@ class local_file_picker(ui.dialog):
             })
         self.grid.update()
 
-    async def handle_double_click(self, msg: dict) -> None:
+    async def handle_double_click(self, msg: Dict) -> None:
         self.path = Path(msg['args']['data']['path'])
         if self.path.is_dir():
             self.update_grid()

+ 109 - 0
examples/simpy/async_realtime_environment.py

@@ -0,0 +1,109 @@
+import asyncio
+from time import monotonic
+from typing import Any, Optional, Union
+
+from numpy import Infinity
+from simpy.core import EmptySchedule, Environment, Infinity, SimTime, StopSimulation
+from simpy.events import URGENT, Event
+from simpy.rt import RealtimeEnvironment
+
+
+class AsyncRealtimeEnvironment(RealtimeEnvironment):
+    """A real-time simulation environment that uses asyncio.
+
+    The methods step and run are a 1-1 copy of the original methods from simpy.rt.RealtimeEnvironment,
+    except that they are async and await asyncio.sleep instead of time.sleep.
+    """
+
+    async def step(self) -> None:
+        """Process the next event after enough real-time has passed for the
+        event to happen.
+
+        The delay is scaled according to the real-time :attr:`factor`. With
+        :attr:`strict` mode enabled, a :exc:`RuntimeError` will be raised, if
+        the event is processed too slowly.
+
+        """
+        evt_time = self.peek()
+
+        if evt_time is Infinity:
+            raise EmptySchedule()
+
+        real_time = self.real_start + (evt_time - self.env_start) * self.factor
+
+        if self.strict and monotonic() - real_time > self.factor:
+            # Events scheduled for time *t* may take just up to *t+1*
+            # for their computation, before an error is raised.
+            delta = monotonic() - real_time
+            raise RuntimeError(
+                f'Simulation too slow for real time ({delta:.3f}s).'
+            )
+
+        # Sleep in a loop to fix inaccuracies of windows (see
+        # http://stackoverflow.com/a/15967564 for details) and to ignore
+        # interrupts.
+        while True:
+            delta = real_time - monotonic()
+            if delta <= 0:
+                break
+            await asyncio.sleep(delta)
+
+        Environment.step(self)
+
+    async def run(
+        self, until: Optional[Union[SimTime, Event]] = None
+    ) -> Optional[Any]:
+        """Executes :meth:`step()` until the given criterion *until* is met.
+
+        - If it is ``None`` (which is the default), this method will return
+          when there are no further events to be processed.
+
+        - If it is an :class:`~simpy.events.Event`, the method will continue
+          stepping until this event has been triggered and will return its
+          value.  Raises a :exc:`RuntimeError` if there are no further events
+          to be processed and the *until* event was not triggered.
+
+        - If it is a number, the method will continue stepping
+          until the environment's time reaches *until*.
+
+        """
+        if until is not None:
+            if not isinstance(until, Event):
+                # Assume that *until* is a number if it is not None and
+                # not an event.  Create a Timeout(until) in this case.
+                at: SimTime
+                if isinstance(until, int):
+                    at = until
+                else:
+                    at = float(until)
+
+                if at <= self.now:
+                    raise ValueError(
+                        f'until(={at}) must be > the current simulation time.'
+                    )
+
+                # Schedule the event before all regular timeouts.
+                until = Event(self)
+                until._ok = True
+                until._value = None
+                self.schedule(until, URGENT, at - self.now)
+
+            elif until.callbacks is None:
+                # Until event has already been processed.
+                return until.value
+
+            until.callbacks.append(StopSimulation.callback)
+
+        try:
+            while True:
+                await self.step()
+        except StopSimulation as exc:
+            return exc.args[0]  # == until.value
+        except EmptySchedule:
+            if until is not None:
+                assert not until.triggered
+                raise RuntimeError(
+                    f'No scheduled events left but "until" event was not '
+                    f'triggered: {until}'
+                )
+        return None

+ 49 - 0
examples/simpy/main.py

@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+import asyncio
+import datetime
+
+from async_realtime_environment import AsyncRealtimeEnvironment
+
+from nicegui import ui
+
+start_time = datetime.datetime.now()
+
+
+def clock(env):
+    while True:
+        simulation_time = start_time + datetime.timedelta(seconds=env.now)
+        clock_label.text = simulation_time.strftime('%H:%M:%S')
+        yield env.timeout(1)
+
+
+def traffic_light(env):
+    while True:
+        light.classes('bg-green-500', remove='bg-red-500')
+        yield env.timeout(30)
+        light.classes('bg-yellow-500', remove='bg-green-500')
+        yield env.timeout(5)
+        light.classes('bg-red-500', remove='bg-yellow-500')
+        yield env.timeout(20)
+
+
+async def run_simpy():
+    env = AsyncRealtimeEnvironment(factor=0.1)  # fast forward simulation with 1/10th of realtime
+    env.process(traffic_light(env))
+    env.process(clock(env))
+    try:
+        await env.run(until=300)  # run until 300 seconds of simulation time have passed
+    except asyncio.CancelledError:
+        return
+    ui.notify('Simulation completed')
+    content.classes('opacity-0')  # fade out the content
+
+# define the UI
+with ui.column().classes('absolute-center items-center transition-opacity duration-500') as content:
+    ui.label('SimPy Traffic Light Demo').classes('text-2xl mb-6')
+    light = ui.element('div').classes('w-10 h-10 rounded-full shadow-lg transition')
+    clock_label = ui.label()
+
+# start the simpy simulation as an async task in the background as soon as the UI is ready
+ui.timer(0, run_simpy, once=True)
+
+ui.run()

+ 2 - 0
examples/simpy/requirements.txt

@@ -0,0 +1,2 @@
+nicegui>=1.2
+simpy

+ 65 - 0
examples/todo_list/main.py

@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+from dataclasses import dataclass
+from typing import List
+
+from nicegui import ui
+
+
+@dataclass
+class TodoItem:
+    name: str
+    done: bool = False
+
+
+items: List[TodoItem] = [
+    TodoItem('Buy milk', done=True),
+    TodoItem('Clean the house'),
+    TodoItem('Call mom'),
+]
+
+
+def add(name: str) -> None:
+    items.append(TodoItem(name))
+    add_input.value = None
+    render_list.refresh()
+
+
+def remove(item: TodoItem) -> None:
+    items.remove(item)
+    render_list.refresh()
+
+
+def toggle(item: TodoItem) -> None:
+    item.done = not item.done
+    render_list.refresh()
+
+
+def rename(item: TodoItem, name: str) -> None:
+    item.name = name
+    render_list.refresh()
+
+
+@ui.refreshable
+def render_list():
+    if not items:
+        ui.label('List is empty.')
+        return
+    ui.linear_progress(sum(item.done for item in items) / len(items), show_value=False)
+    with ui.row().classes('justify-center w-full'):
+        ui.label(f'Completed: {sum(item.done for item in items)}')
+        ui.label(f'Remaining: {sum(not item.done for item in items)}')
+    for item in items:
+        with ui.row().classes('items-center'):
+            ui.checkbox(value=item.done, on_change=lambda _, item=item: toggle(item))
+            input = ui.input(value=item.name).classes('flex-grow')
+            input.on('keydown.enter', lambda _, item=item, input=input: rename(item, input.value))
+            ui.button(on_click=lambda _, item=item: remove(item)).props('flat fab-mini icon=delete color=grey')
+
+
+with ui.card().classes('w-80 items-stretch'):
+    ui.label('Todo list').classes('text-semibold text-2xl')
+    render_list()
+    add_input = ui.input('New item').classes('mx-12')
+    add_input.on('keydown.enter', lambda: add(add_input.value))
+
+ui.run()

+ 17 - 0
fetch_dependencies.py

@@ -56,6 +56,23 @@ css = request_buffered_str(url)
 Path('nicegui/static/quasar.prod.css').write_text(css)
 print('Quasar:', version)
 
+# Quasar language packs
+url = 'https://cdn.jsdelivr.net/npm/quasar@2/dist/lang/'
+html = request_buffered_str(url)
+soup = BeautifulSoup(html, 'html.parser')
+languages = []
+for link in soup.find_all('a', href=re.compile(r'\.umd\.prod\.js$')):
+    name = link.get('href').split('/')[-1]
+    languages.append(name.split('.')[0])
+    js = request_buffered_str(url + name)
+    Path(f'nicegui/static/quasar.{name}').write_text(js)
+with open(Path(__file__).parent / 'nicegui' / 'language.py', 'w') as f:
+    f.write(f'from typing_extensions import Literal\n\n')
+    f.write(f'Language = Literal[\n')
+    for language in languages:
+        f.write(f"    '{language}',\n")
+    f.write(f']\n')
+
 # vue.js
 url = 'https://unpkg.com/vue@3/anything'
 info = request_buffered_str(url)

+ 6 - 4
fetch_tailwind.py

@@ -2,17 +2,19 @@
 import re
 from dataclasses import dataclass, field
 from pathlib import Path
+from typing import List
 
 import requests
 from bs4 import BeautifulSoup
+from secure import SecurePath
 
 
 @dataclass
 class Property:
     title: str
     description: str
-    members: list[str]
-    short_members: list[str] = field(init=False)
+    members: List[str]
+    short_members: List[str] = field(init=False)
     common_prefix: str = field(init=False)
 
     def __post_init__(self) -> None:
@@ -47,7 +49,7 @@ class Property:
         return '_'.join(word.lower() for word in re.sub(r'[-/ &]', ' ', self.title).split())
 
 
-properties: list[Property] = []
+properties: List[Property] = []
 
 
 def get_soup(url: str) -> BeautifulSoup:
@@ -81,7 +83,7 @@ for file in (Path(__file__).parent / 'nicegui' / 'tailwind_types').glob('*.py'):
 for property in properties:
     if not property.members:
         continue
-    with open(Path(__file__).parent / 'nicegui' / 'tailwind_types' / f'{property.snake_title}.py', 'w') as f:
+    with SecurePath(open(Path(__file__).parent / 'nicegui' / 'tailwind_types' / f'{property.snake_title}.py', 'w')) as f:
         f.write('from typing_extensions import Literal\n')
         f.write('\n')
         f.write(f'{property.pascal_title} = Literal[\n')

+ 1 - 1
fly.dockerfile

@@ -1,4 +1,4 @@
-FROM python:3.11-slim
+FROM python:3.11.3-slim
 
 LABEL maintainer="Zauberzeug GmbH <nicegui@zauberzeug.com>"
 

+ 39 - 31
main.py

@@ -10,10 +10,10 @@ if True:
 
 import os
 from pathlib import Path
-from typing import Optional
+from typing import Awaitable, Callable, Optional
 
 from fastapi import Request
-from fastapi.responses import FileResponse, RedirectResponse
+from fastapi.responses import FileResponse, RedirectResponse, Response
 from starlette.middleware.sessions import SessionMiddleware
 
 import prometheus
@@ -29,24 +29,25 @@ from website.style import example_link, features, heading, link_target, section_
 prometheus.start_monitor(app)
 
 # session middleware is required for demo in documentation and prometheus
-app.add_middleware(SessionMiddleware, secret_key='NiceGUI is awesome!')
+app.add_middleware(SessionMiddleware, secret_key=os.environ.get('NICEGUI_SECRET_KEY', ''))
 
 app.add_static_files('/favicon', str(Path(__file__).parent / 'website' / 'favicon'))
 app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 
 
 @app.get('/logo.png')
-def logo():
+def logo() -> FileResponse:
     return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
 
 
 @app.get('/logo_square.png')
-def logo():
+def logo_square() -> FileResponse:
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
 
 
 @app.middleware('http')
-async def redirect_reference_to_documentation(request: Request, call_next):
+async def redirect_reference_to_documentation(request: Request,
+                                              call_next: Callable[[Request], Awaitable[Response]]) -> Response:
     if request.url.path == '/reference':
         return RedirectResponse('/documentation')
     return await call_next(request)
@@ -75,19 +76,20 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
             .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).props('flat color=white icon=menu round') \
-                .classes('max-[405px]:hidden lg:hidden')
+            ui.button(on_click=menu.toggle).props('flat color=white icon=menu 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')
             svg.word().classes('w-24')
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
-        with ui.link(target='https://discord.gg/TEpFeAaF4f'):
+        with ui.link(target='https://discord.gg/TEpFeAaF4f').classes('max-[435px]:hidden').tooltip('Discord'):
             svg.discord().classes('fill-white scale-125 m-1')
-        with ui.link(target='https://github.com/zauberzeug/nicegui/'):
+        with ui.link(target='https://www.reddit.com/r/nicegui/').classes('max-[385px]:hidden').tooltip('Reddit'):
+            svg.reddit().classes('fill-white scale-125 m-1')
+        with ui.link(target='https://github.com/zauberzeug/nicegui/').tooltip('GitHub'):
             svg.github().classes('fill-white scale-125 m-1')
-        add_star().classes('max-[460px]:hidden')
+        add_star().classes('max-[480px]:hidden')
         with ui.row().classes('lg:hidden'):
             with ui.button().props('flat color=white icon=more_vert round'):
                 with ui.menu().classes('bg-primary text-white text-lg').props(remove='no-parent-event'):
@@ -96,7 +98,7 @@ def add_header(menu: Optional[ui.left_drawer] = None) -> None:
 
 
 @ui.page('/')
-async def index_page(client: Client):
+async def index_page(client: Client) -> None:
     client.content.classes('p-0 gap-0')
     add_head_html()
     add_header()
@@ -119,17 +121,21 @@ async def index_page(client: Client):
         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 handles all the web development details for you. '
-                    'So you can focus on writing Python code. '
-                    'Anything from short scripts and dashboards to full robotics projects, IoT solutions, '
-                    'smart home automations and machine learning projects can benefit from having all code in one place.'
-                )
-                ui.markdown(
-                    '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).')
+                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'):
@@ -187,12 +193,12 @@ async def index_page(client: Client):
             features('swap_horiz', 'Interaction', [
                 'buttons, switches, sliders, inputs, ...',
                 'notifications, dialogs and menus',
-                'keyboard input',
-                'on-screen joystick',
+                'interactive images with SVG overlays',
+                'web pages and native window apps',
             ])
             features('space_dashboard', 'Layout', [
                 'navigation bars, tabs, panels, ...',
-                'grouping with rows, columns and cards',
+                'grouping with rows, columns, grids and cards',
                 'HTML and Markdown elements',
                 'flex layout by default',
             ])
@@ -209,8 +215,8 @@ async def index_page(client: Client):
                 '[Tailwind CSS](https://tailwindcss.com/) auto-completion',
             ])
             features('source', 'Coding', [
-                'live-cycle events',
-                'implicit reload on code change',
+                'routing for multiple pages',
+                'auto-reload on code change',
                 'straight-forward data binding',
                 'Jupyter notebook compatibility',
             ])
@@ -264,6 +270,7 @@ async def index_page(client: Client):
             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')
@@ -312,7 +319,7 @@ async def index_page(client: Client):
 
 
 @ui.page('/documentation')
-def documentation_page():
+def documentation_page() -> None:
     add_head_html()
     menu = side_menu()
     add_header(menu)
@@ -327,7 +334,7 @@ def documentation_page():
 
 
 @ui.page('/documentation/{name}')
-def documentation_page_more(name: str):
+async def documentation_page_more(name: str, client: Client) -> None:
     if not hasattr(ui, name):
         name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
     module = importlib.import_module(f'website.more_documentation.{name}_documentation')
@@ -355,6 +362,7 @@ def documentation_page_more(name: str):
                 ui.markdown('**Reference**').classes('mt-4')
             ui.markdown('## Reference').classes('mt-16')
             generate_class_doc(api)
-
+    await client.connected()
+    await ui.run_javascript(f'document.title = "{name} • NiceGUI";', respond=False)
 
 ui.run(uvicorn_reload_includes='*.py, *.css, *.html')

+ 10 - 0
nicegui/__init__.py

@@ -9,3 +9,13 @@ from . import elements, globals, ui
 from .client import Client
 from .nicegui import app
 from .tailwind import Tailwind
+
+__all__ = [
+    'app',
+    'Client',
+    'elements',
+    'globals',
+    'Tailwind',
+    'ui',
+    '__version__',
+]

+ 5 - 5
nicegui/app.py

@@ -60,7 +60,7 @@ class App(FastAPI):
             raise Exception('calling shutdown() is not supported when auto-reload is enabled')
         globals.server.should_exit = True
 
-    def add_static_files(self, path: str, directory: str) -> None:
+    def add_static_files(self, url_path: str, local_directory: str) -> None:
         """Add static files.
 
         `add_static_files()` makes a local directory available at the specified endpoint, e.g. `'/static'`.
@@ -68,12 +68,12 @@ class App(FastAPI):
         Otherwise the browser would not be able to access the files.
         Do only put non-security-critical files in there, as they are accessible to everyone.
 
-        :param path: string that starts with a slash "/"
-        :param directory: folder with static files to serve under the given path
+        :param url_path: string that starts with a slash "/" and identifies the path at which the files should be served
+        :param local_directory: local folder with files to serve as static content
         """
-        if path == '/':
+        if url_path == '/':
             raise ValueError('''Path cannot be "/", because it would hide NiceGUI's internal "/_nicegui" route.''')
-        globals.app.mount(path, StaticFiles(directory=directory))
+        globals.app.mount(url_path, StaticFiles(directory=local_directory))
 
     def remove_route(self, path: str) -> None:
         """Remove routes with the given path."""

+ 6 - 6
nicegui/background_tasks.py

@@ -1,4 +1,4 @@
-'''inspired from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/'''
+"""inspired from https://quantlane.com/blog/ensure-asyncio-task-exceptions-get-logged/"""
 import asyncio
 import sys
 from typing import Awaitable, Dict, Set, TypeVar
@@ -15,12 +15,12 @@ lazy_tasks_waiting: Dict[str, Awaitable[T]] = {}
 
 
 def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.Task[T]':
-    '''Wraps a loop.create_task call and ensures there is an exception handler added to the task.
+    """Wraps a loop.create_task call and ensures there is an exception handler added to the task.
 
     If the task raises an exception, it is logged and handled by the global exception handlers.
     Also a reference to the task is kept until it is done, so that the task is not garbage collected mid-execution.
     See https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task.
-    '''
+    """
     task = globals.loop.create_task(coroutine, name=name) if name_supported else globals.loop.create_task(coroutine)
     task.add_done_callback(_handle_task_result)
     running_tasks.add(task)
@@ -28,11 +28,11 @@ def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.T
     return task
 
 
-def create_lazy(coroutine: Awaitable[T], *, name: str) -> 'asyncio.Task[T]':
-    '''Wraps a create call and ensures a second task with the same name is delayed until the first one is done.
+def create_lazy(coroutine: Awaitable[T], *, name: str) -> None:
+    """Wraps a create call and ensures a second task with the same name is delayed until the first one is done.
 
     If a third task with the same name is created while the first one is still running, the second one is discarded.
-    '''
+    """
     if name in lazy_tasks_running:
         lazy_tasks_waiting[name] = coroutine
         return

+ 5 - 5
nicegui/binding.py

@@ -8,7 +8,7 @@ from . import globals
 
 bindings: DefaultDict[Tuple[int, str], List] = defaultdict(list)
 bindable_properties: Dict[Tuple[int, str], Any] = {}
-active_links: List[Tuple[Any, str, Any, str, Callable]] = []
+active_links: List[Tuple[Any, str, Any, str, Callable[[Any], Any]]] = []
 
 
 def get_attribute(obj: Union[object, Dict], name: str) -> Any:
@@ -54,14 +54,14 @@ def propagate(source_obj: Any, source_name: str, visited: Optional[Set[Tuple[int
             propagate(target_obj, target_name, visited)
 
 
-def bind_to(self_obj: Any, self_name: str, other_obj: Any, other_name: str, forward: Callable) -> None:
+def bind_to(self_obj: Any, self_name: str, other_obj: Any, other_name: str, forward: Callable[[Any], Any]) -> None:
     bindings[(id(self_obj), self_name)].append((self_obj, other_obj, other_name, forward))
     if (id(self_obj), self_name) not in bindable_properties:
         active_links.append((self_obj, self_name, other_obj, other_name, forward))
     propagate(self_obj, self_name)
 
 
-def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, backward: Callable) -> None:
+def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, backward: Callable[[Any], Any]) -> None:
     bindings[(id(other_obj), other_name)].append((other_obj, self_obj, self_name, backward))
     if (id(other_obj), other_name) not in bindable_properties:
         active_links.append((other_obj, other_name, self_obj, self_name, backward))
@@ -69,14 +69,14 @@ def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, ba
 
 
 def bind(self_obj: Any, self_name: str, other_obj: Any, other_name: str, *,
-         forward: Callable = lambda x: x, backward: Callable = lambda x: x) -> None:
+         forward: Callable[[Any], Any] = lambda x: x, backward: Callable[[Any], Any] = lambda x: x) -> None:
     bind_from(self_obj, self_name, other_obj, other_name, backward=backward)
     bind_to(self_obj, self_name, other_obj, other_name, forward=forward)
 
 
 class BindableProperty:
 
-    def __init__(self, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, on_change: Optional[Callable[..., Any]] = None) -> None:
         self.on_change = on_change
 
     def __set_name__(self, _, name: str) -> None:

+ 7 - 6
nicegui/client.py

@@ -47,8 +47,8 @@ class Client:
 
         self.page = page
 
-        self.connect_handlers: List[Union[Callable, Awaitable]] = []
-        self.disconnect_handlers: List[Union[Callable, Awaitable]] = []
+        self.connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+        self.disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
 
     @property
     def ip(self) -> Optional[str]:
@@ -84,6 +84,7 @@ class Client:
             'viewport': self.page.resolve_viewport(),
             'favicon_url': get_favicon_url(self.page, prefix),
             'dark': str(self.page.resolve_dark()),
+            'language': self.page.resolve_language(),
             'prefix': prefix,
             'tailwind': globals.tailwind,
             'socket_io_js_extra_headers': globals.socket_io_js_extra_headers,
@@ -111,7 +112,7 @@ class Client:
         """Execute JavaScript on the client.
 
         The client connection must be established before this method is called.
-        You can do this by `await client.connected()` or register a callback with `client.on_connected(...)`.
+        You can do this by `await client.connected()` or register a callback with `client.on_connect(...)`.
         If respond is True, the javascript code must return a string.
         """
         request_id = str(uuid.uuid4())
@@ -129,7 +130,7 @@ class Client:
             await asyncio.sleep(check_interval)
         return self.waiting_javascript_commands.pop(request_id)
 
-    def open(self, target: Union[Callable, str]) -> None:
+    def open(self, target: Union[Callable[..., Any], str]) -> None:
         """Open a new page in the client."""
         path = target if isinstance(target, str) else globals.page_routes[target]
         outbox.enqueue_message('open', path, self.id)
@@ -138,10 +139,10 @@ class Client:
         """Download a file from the given URL."""
         outbox.enqueue_message('download', {'url': url, 'filename': filename}, self.id)
 
-    def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
+    def on_connect(self, handler: Union[Callable[..., Any], Awaitable]) -> None:
         """Register a callback to be called when the client connects."""
         self.connect_handlers.append(handler)
 
-    def on_disconnect(self, handler: Union[Callable, Awaitable]) -> None:
+    def on_disconnect(self, handler: Union[Callable[..., Any], Awaitable]) -> None:
         """Register a callback to be called when the client disconnects."""
         self.disconnect_handlers.append(handler)

+ 14 - 10
nicegui/element.py

@@ -3,7 +3,7 @@ from __future__ import annotations
 import re
 import warnings
 from copy import deepcopy
-from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Union
 
 from typing_extensions import Self
 
@@ -18,7 +18,7 @@ from .tailwind import Tailwind
 if TYPE_CHECKING:
     from .client import Client
 
-PROPS_PATTERN = re.compile(r'([\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
+PROPS_PATTERN = re.compile(r'([:\w\-]+)(?:=(?:("[^"\\]*(?:\\.[^"\\]*)*")|([\w\-.%:\/]+)))?(?:$|\s)')
 
 
 class Element(Visibility):
@@ -39,7 +39,7 @@ class Element(Visibility):
         self.tag = tag
         self._classes: List[str] = []
         self._style: Dict[str, str] = {}
-        self._props: Dict[str, Any] = {}
+        self._props: Dict[str, Any] = {'key': self.id}  # HACK: workaround for #600 and #898
         self._event_listeners: Dict[str, EventListener] = {}
         self._text: Optional[str] = None
         self.slots: Dict[str, Slot] = {}
@@ -75,9 +75,14 @@ class Element(Visibility):
     def __exit__(self, *_):
         self.default_slot.__exit__(*_)
 
+    def __iter__(self) -> Iterator[Element]:
+        for slot in self.slots.values():
+            for child in slot:
+                yield child
+
     def _collect_slot_dict(self) -> Dict[str, List[int]]:
         return {
-            name: {'template': slot.template, 'ids': [child.id for child in slot.children]}
+            name: {'template': slot.template, 'ids': [child.id for child in slot]}
             for name, slot in self.slots.items()
         }
 
@@ -197,7 +202,7 @@ class Element(Visibility):
 
     def on(self,
            type: str,
-           handler: Optional[Callable],
+           handler: Optional[Callable[..., Any]] = None,
            args: Optional[List[str]] = None, *,
            throttle: float = 0.0,
            leading_events: bool = True,
@@ -251,9 +256,8 @@ class Element(Visibility):
 
     def _collect_descendant_ids(self) -> List[int]:
         ids: List[int] = [self.id]
-        for slot in self.slots.values():
-            for child in slot.children:
-                ids.extend(child._collect_descendant_ids())
+        for child in self:
+            ids.extend(child._collect_descendant_ids())
         return ids
 
     def clear(self) -> None:
@@ -286,12 +290,12 @@ class Element(Visibility):
         :param element: either the element instance or its ID
         """
         if isinstance(element, int):
-            children = [child for slot in self.slots.values() for child in slot.children]
+            children = list(self)
             element = children[element]
         binding.remove([element], Element)
         del self.client.elements[element.id]
         for slot in self.slots.values():
-            slot.children[:] = [e for e in slot.children if e.id != element.id]
+            slot.children[:] = [e for e in slot if e.id != element.id]
         self.update()
 
     def delete(self) -> None:

+ 0 - 1
nicegui/elements/aggrid.py

@@ -25,7 +25,6 @@ class AgGrid(Element):
         super().__init__('aggrid')
         self._props['options'] = options
         self._props['html_columns'] = html_columns
-        self._props['key'] = self.id  # HACK: workaround for #600
         self._classes = ['nicegui-aggrid', f'ag-theme-{theme}']
 
     @staticmethod

+ 3 - 2
nicegui/elements/avatar.py

@@ -7,7 +7,7 @@ from ..element import Element
 class Avatar(Element):
 
     def __init__(self,
-                 icon: str = 'none', *,
+                 icon: Optional[str] = None, *,
                  color: Optional[str] = 'primary',
                  text_color: Optional[str] = None,
                  size: Optional[str] = None,
@@ -30,7 +30,8 @@ class Avatar(Element):
         """
         super().__init__('q-avatar')
 
-        self._props['icon'] = icon
+        if icon is not None:
+            self._props['icon'] = icon
         self._props['square'] = square
         self._props['rounded'] = rounded
 

+ 2 - 2
nicegui/elements/button.py

@@ -1,5 +1,5 @@
 import asyncio
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from ..colors import set_background_color
 from ..events import ClickEventArguments, handle_event
@@ -11,7 +11,7 @@ class Button(TextElement, DisableableElement):
 
     def __init__(self,
                  text: str = '', *,
-                 on_click: Optional[Callable] = None,
+                 on_click: Optional[Callable[..., Any]] = None,
                  color: Optional[str] = 'primary',
                  ) -> None:
         """Button

+ 12 - 5
nicegui/elements/chart.js

@@ -4,6 +4,7 @@ export default {
     setTimeout(() => {
       const imports = this.extras.map((extra) => import(window.path_prefix + extra));
       Promise.allSettled(imports).then(() => {
+        this.seriesCount = this.options.series ? this.options.series.length : 0;
         this.chart = Highcharts[this.type](this.$el, this.options);
         this.chart.reflow();
       });
@@ -18,16 +19,22 @@ export default {
   methods: {
     update_chart() {
       if (this.chart) {
-        while (this.chart.series.length > this.options.series.length) this.chart.series[0].remove();
-        while (this.chart.series.length < this.options.series.length) this.chart.addSeries({}, false);
+        while (this.seriesCount > this.options.series.length) {
+          this.chart.series[0].remove();
+          this.seriesCount--;
+        }
+        while (this.seriesCount < this.options.series.length) {
+          this.chart.addSeries({}, false);
+          this.seriesCount++;
+        }
         this.chart.update(this.options);
       }
     },
-    destroyChart () {
+    destroyChart() {
       if (this.chart) {
-        this.chart.destroy()
+        this.chart.destroy();
       }
-    }
+    },
   },
   props: {
     type: String,

+ 0 - 1
nicegui/elements/chart.py

@@ -106,7 +106,6 @@ class Chart(Element):
             for dependency in js_dependencies.values()
             if dependency.optional and dependency.path.stem in extras and 'chart' in dependency.dependents
         ]
-        self._props['key'] = self.id  # HACK: workaround for #600
 
     @property
     def options(self) -> Dict:

+ 2 - 2
nicegui/elements/checkbox.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
@@ -7,7 +7,7 @@ from .mixins.value_element import ValueElement
 
 class Checkbox(TextElement, ValueElement, DisableableElement):
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
         """Checkbox
 
         :param text: the label to display next to the checkbox

+ 5 - 1
nicegui/elements/choice_element.py

@@ -6,7 +6,11 @@ from .mixins.value_element import ValueElement
 class ChoiceElement(ValueElement):
 
     def __init__(self, *,
-                 tag: str, options: Union[List, Dict], value: Any, on_change: Optional[Callable] = None) -> None:
+                 tag: str,
+                 options: Union[List, Dict],
+                 value: Any,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         self.options = options
         self._values: List[str] = []
         self._labels: List[str] = []

+ 8 - 4
nicegui/elements/color_input.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from nicegui import ui
 
@@ -10,14 +10,18 @@ from .mixins.value_element import ValueElement
 class ColorInput(ValueElement, DisableableElement):
     LOOPBACK = False
 
-    def __init__(self, label: Optional[str] = None, *,
-                 placeholder: Optional[str] = None, value: str = '', on_change: Optional[Callable] = None) -> None:
+    def __init__(self,
+                 label: Optional[str] = None, *,
+                 placeholder: Optional[str] = None,
+                 value: str = '',
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Color Input
 
         :param label: displayed label for the color input
         :param placeholder: text to show if no color is selected
         :param value: the current color value
-        :param on_change: callback to execute when the input is confirmed by leaving the focus
+        :param on_change: callback to execute when the value changes
         """
         super().__init__(tag='q-input', value=value, on_value_change=on_change)
         if label is not None:

+ 2 - 2
nicegui/elements/color_picker.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict
+from typing import Any, Callable, Dict
 
 from nicegui.events import ColorPickEventArguments, handle_event
 
@@ -8,7 +8,7 @@ from .menu import Menu
 
 class ColorPicker(Menu):
 
-    def __init__(self, *, on_pick: Callable, value: bool = False) -> None:
+    def __init__(self, *, on_pick: Callable[..., Any], value: bool = False) -> None:
         """Color Picker
 
         :param on_pick: callback to execute when a color is picked

+ 0 - 1
nicegui/elements/colors.py

@@ -28,5 +28,4 @@ class Colors(Element):
         self._props['negative'] = negative
         self._props['info'] = info
         self._props['warning'] = warning
-        self._props['key'] = self.id  # HACK: workaround for #600
         self.update()

+ 2 - 2
nicegui/elements/date.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,7 @@ class Date(ValueElement, DisableableElement):
                  value: Optional[str] = None,
                  *,
                  mask: str = 'YYYY-MM-DD',
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None) -> None:
         """Date Input
 
         This element is based on Quasar's `QDate <https://quasar.dev/vue-components/date>`_ component.

+ 1 - 1
nicegui/elements/input.py

@@ -14,7 +14,7 @@ class Input(ValueElement, DisableableElement):
                  value: str = '',
                  password: bool = False,
                  password_toggle_button: bool = False,
-                 on_change: Optional[Callable] = None,
+                 on_change: Optional[Callable[..., Any]] = None,
                  autocomplete: Optional[List[str]] = None,
                  validation: Optional[Dict[str, Callable[[Any], bool]]] = None) -> None:
         """Text Input

+ 7 - 3
nicegui/elements/interactive_image.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from typing import Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 from ..dependencies import register_component
 from ..events import MouseEventArguments, handle_event
@@ -13,9 +13,13 @@ register_component('interactive_image', __file__, 'interactive_image.js')
 class InteractiveImage(SourceElement, ContentElement):
     CONTENT_PROP = 'content'
 
-    def __init__(self, source: str = '', *,
+    def __init__(self,
+                 source: str = '', *,
                  content: str = '',
-                 on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False) -> None:
+                 on_mouse: Optional[Callable[..., Any]] = None,
+                 events: List[str] = ['click'],
+                 cross: bool = False,
+                 ) -> None:
         """Interactive Image
 
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.

+ 3 - 3
nicegui/elements/joystick.py

@@ -10,9 +10,9 @@ register_component('joystick', __file__, 'joystick.vue', ['lib/nipplejs.min.js']
 class Joystick(Element):
 
     def __init__(self, *,
-                 on_start: Optional[Callable] = None,
-                 on_move: Optional[Callable] = None,
-                 on_end: Optional[Callable] = None,
+                 on_start: Optional[Callable[..., Any]] = None,
+                 on_move: Optional[Callable[..., Any]] = None,
+                 on_end: Optional[Callable[..., Any]] = None,
                  throttle: float = 0.05,
                  ** options: Any) -> None:
         """Joystick

+ 2 - 2
nicegui/elements/keyboard.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, List
+from typing import Any, Callable, Dict, List
 
 from typing_extensions import Literal
 
@@ -14,7 +14,7 @@ class Keyboard(Element):
     active = BindableProperty()
 
     def __init__(self,
-                 on_key: Callable, *,
+                 on_key: Callable[..., Any], *,
                  active: bool = True,
                  repeating: bool = True,
                  ignore: List[Literal['input', 'select', 'button', 'textarea']] = ['input', 'select', 'button', 'textarea'],

+ 9 - 3
nicegui/elements/line_plot.py

@@ -1,11 +1,17 @@
-from typing import List
+from typing import Any, List
 
 from .pyplot import Pyplot
 
 
 class LinePlot(Pyplot):
 
-    def __init__(self, *, n: int = 1, limit: int = 100, update_every: int = 1, close: bool = True, **kwargs) -> None:
+    def __init__(self, *,
+                 n: int = 1,
+                 limit: int = 100,
+                 update_every: int = 1,
+                 close: bool = True,
+                 **kwargs: Any,
+                 ) -> None:
         """Line Plot
 
         Create a line plot using pyplot.
@@ -26,7 +32,7 @@ class LinePlot(Pyplot):
         self.update_every = update_every
         self.push_counter = 0
 
-    def with_legend(self, titles: List[str], **kwargs):
+    def with_legend(self, titles: List[str], **kwargs: Any):
         self.fig.gca().legend(titles, **kwargs)
         self._convert_to_html()
         return self

+ 2 - 3
nicegui/elements/link.py

@@ -1,4 +1,4 @@
-from typing import Callable, Union
+from typing import Any, Callable, Union
 
 from .. import globals
 from ..dependencies import register_component
@@ -10,7 +10,7 @@ register_component('link', __file__, 'link.js')
 
 class Link(TextElement):
 
-    def __init__(self, text: str = '', target: Union[Callable, str] = '#', new_tab: bool = False) -> None:
+    def __init__(self, text: str = '', target: Union[Callable[..., Any], str] = '#', new_tab: bool = False) -> None:
         """Link
 
         Create a hyperlink.
@@ -25,7 +25,6 @@ class Link(TextElement):
         super().__init__(tag='link', text=text)
         self._props['href'] = target if isinstance(target, str) else globals.page_routes[target]
         self._props['target'] = '_blank' if new_tab else '_self'
-        self._props['key'] = self.id  # HACK: workaround for #600
         self._classes = ['nicegui-link']
 
 

+ 1 - 2
nicegui/elements/log.py

@@ -20,14 +20,13 @@ class Log(Element):
         super().__init__('log')
         self._props['max_lines'] = max_lines
         self._props['lines'] = ''
-        self._props['key'] = self.id  # HACK: workaround for #600
         self._classes = ['nicegui-log']
         self.lines: deque[str] = deque(maxlen=max_lines)
 
     def push(self, line: Any) -> None:
         self.lines.extend(map(urllib.parse.quote, str(line).splitlines()))
         self._props['lines'] = '\n'.join(self.lines)
-        self.run_method('push', line)
+        self.run_method('push', urllib.parse.quote(str(line)))
 
     def clear(self) -> None:
         """Clear the log"""

+ 6 - 2
nicegui/elements/menu.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .. import globals
 from ..events import ClickEventArguments, handle_event
@@ -28,7 +28,11 @@ class Menu(ValueElement):
 
 class MenuItem(TextElement):
 
-    def __init__(self, text: str = '', on_click: Optional[Callable] = None, *, auto_close: bool = True) -> None:
+    def __init__(self,
+                 text: str = '',
+                 on_click: Optional[Callable[..., Any]] = None, *,
+                 auto_close: bool = True,
+                 ) -> None:
         """Menu Item
 
         A menu item to be added to a menu.

+ 8 - 5
nicegui/elements/mixins/content_element.py

@@ -10,7 +10,7 @@ class ContentElement(Element):
     CONTENT_PROP = 'innerHTML'
     content = BindableProperty(on_change=lambda sender, content: sender.on_content_change(content))
 
-    def __init__(self, *, content: str, **kwargs) -> None:
+    def __init__(self, *, content: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.content = content
         self.on_content_change(content)
@@ -18,7 +18,8 @@ class ContentElement(Element):
     def bind_content_to(self,
                         target_object: Any,
                         target_name: str = 'content',
-                        forward: Callable = lambda x: x) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the content of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -33,7 +34,8 @@ class ContentElement(Element):
     def bind_content_from(self,
                           target_object: Any,
                           target_name: str = 'content',
-                          backward: Callable = lambda x: x) -> Self:
+                          backward: Callable[..., Any] = lambda x: x,
+                          ) -> Self:
         """Bind the content of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -48,8 +50,9 @@ class ContentElement(Element):
     def bind_content(self,
                      target_object: Any,
                      target_name: str = 'content', *,
-                     forward: Callable = lambda x: x,
-                     backward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     backward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the content of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/disableable_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class DisableableElement(Element):
     enabled = BindableProperty(on_change=lambda sender, value: sender.on_enabled_change(value))
 
-    def __init__(self, **kwargs) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.enabled = True
 
@@ -24,7 +24,8 @@ class DisableableElement(Element):
     def bind_enabled_to(self,
                         target_object: Any,
                         target_name: str = 'enabled',
-                        forward: Callable = lambda x: x) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the enabled state of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -39,7 +40,8 @@ class DisableableElement(Element):
     def bind_enabled_from(self,
                           target_object: Any,
                           target_name: str = 'enabled',
-                          backward: Callable = lambda x: x) -> Self:
+                          backward: Callable[..., Any] = lambda x: x,
+                          ) -> Self:
         """Bind the enabled state of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -54,8 +56,9 @@ class DisableableElement(Element):
     def bind_enabled(self,
                      target_object: Any,
                      target_name: str = 'enabled', *,
-                     forward: Callable = lambda x: x,
-                     backward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     backward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the enabled state of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/filter_element.py

@@ -10,7 +10,7 @@ class FilterElement(Element):
     FILTER_PROP = 'filter'
     filter = BindableProperty(on_change=lambda sender, filter: sender.on_filter_change(filter))
 
-    def __init__(self, *, filter: Optional[str] = None, **kwargs) -> None:
+    def __init__(self, *, filter: Optional[str] = None, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.filter = filter
         self._props[self.FILTER_PROP] = filter
@@ -18,7 +18,8 @@ class FilterElement(Element):
     def bind_filter_to(self,
                        target_object: Any,
                        target_name: str = 'filter',
-                       forward: Callable = lambda x: x) -> Self:
+                       forward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the filter of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -33,7 +34,8 @@ class FilterElement(Element):
     def bind_filter_from(self,
                          target_object: Any,
                          target_name: str = 'filter',
-                         backward: Callable = lambda x: x) -> Self:
+                         backward: Callable[..., Any] = lambda x: x,
+                         ) -> Self:
         """Bind the filter of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -48,8 +50,9 @@ class FilterElement(Element):
     def bind_filter(self,
                     target_object: Any,
                     target_name: str = 'filter', *,
-                    forward: Callable = lambda x: x,
-                    backward: Callable = lambda x: x) -> Self:
+                    forward: Callable[..., Any] = lambda x: x,
+                    backward: Callable[..., Any] = lambda x: x,
+                    ) -> Self:
         """Bind the filter of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/source_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class SourceElement(Element):
     source = BindableProperty(on_change=lambda sender, source: sender.on_source_change(source))
 
-    def __init__(self, *, source: str, **kwargs) -> None:
+    def __init__(self, *, source: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.source = source
         self._props['src'] = source
@@ -17,7 +17,8 @@ class SourceElement(Element):
     def bind_source_to(self,
                        target_object: Any,
                        target_name: str = 'source',
-                       forward: Callable = lambda x: x) -> Self:
+                       forward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the source of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -32,7 +33,8 @@ class SourceElement(Element):
     def bind_source_from(self,
                          target_object: Any,
                          target_name: str = 'source',
-                         backward: Callable = lambda x: x) -> Self:
+                         backward: Callable[..., Any] = lambda x: x,
+                         ) -> Self:
         """Bind the source of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -47,8 +49,9 @@ class SourceElement(Element):
     def bind_source(self,
                     target_object: Any,
                     target_name: str = 'source', *,
-                    forward: Callable = lambda x: x,
-                    backward: Callable = lambda x: x) -> Self:
+                    forward: Callable[..., Any] = lambda x: x,
+                    backward: Callable[..., Any] = lambda x: x,
+                    ) -> Self:
         """Bind the source of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 8 - 5
nicegui/elements/mixins/text_element.py

@@ -9,7 +9,7 @@ from ...element import Element
 class TextElement(Element):
     text = BindableProperty(on_change=lambda sender, text: sender.on_text_change(text))
 
-    def __init__(self, *, text: str, **kwargs) -> None:
+    def __init__(self, *, text: str, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.text = text
         self._text_to_model_text(text)
@@ -17,7 +17,8 @@ class TextElement(Element):
     def bind_text_to(self,
                      target_object: Any,
                      target_name: str = 'text',
-                     forward: Callable = lambda x: x) -> Self:
+                     forward: Callable[..., Any] = lambda x: x,
+                     ) -> Self:
         """Bind the text of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -32,7 +33,8 @@ class TextElement(Element):
     def bind_text_from(self,
                        target_object: Any,
                        target_name: str = 'text',
-                       backward: Callable = lambda x: x) -> Self:
+                       backward: Callable[..., Any] = lambda x: x,
+                       ) -> Self:
         """Bind the text of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -47,8 +49,9 @@ class TextElement(Element):
     def bind_text(self,
                   target_object: Any,
                   target_name: str = 'text', *,
-                  forward: Callable = lambda x: x,
-                  backward: Callable = lambda x: x) -> Self:
+                  forward: Callable[..., Any] = lambda x: x,
+                  backward: Callable[..., Any] = lambda x: x,
+                  ) -> Self:
         """Bind the text of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 13 - 5
nicegui/elements/mixins/value_element.py

@@ -13,7 +13,12 @@ class ValueElement(Element):
     LOOPBACK = True
     value = BindableProperty(on_change=lambda sender, value: sender.on_value_change(value))
 
-    def __init__(self, *, value: Any, on_value_change: Optional[Callable], throttle: float = 0, **kwargs) -> None:
+    def __init__(self, *,
+                 value: Any,
+                 on_value_change: Optional[Callable[..., Any]],
+                 throttle: float = 0,
+                 **kwargs: Any,
+                 ) -> None:
         super().__init__(**kwargs)
         self.set_value(value)
         self._props[self.VALUE_PROP] = self._value_to_model_value(value)
@@ -30,7 +35,8 @@ class ValueElement(Element):
     def bind_value_to(self,
                       target_object: Any,
                       target_name: str = 'value',
-                      forward: Callable = lambda x: x) -> Self:
+                      forward: Callable[..., Any] = lambda x: x,
+                      ) -> Self:
         """Bind the value of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -45,7 +51,8 @@ class ValueElement(Element):
     def bind_value_from(self,
                         target_object: Any,
                         target_name: str = 'value',
-                        backward: Callable = lambda x: x) -> Self:
+                        backward: Callable[..., Any] = lambda x: x,
+                        ) -> Self:
         """Bind the value of this element from the target object's target_name property.
 
         The binding works one way only, from the target to this element.
@@ -60,8 +67,9 @@ class ValueElement(Element):
     def bind_value(self,
                    target_object: Any,
                    target_name: str = 'value', *,
-                   forward: Callable = lambda x: x,
-                   backward: Callable = lambda x: x) -> Self:
+                   forward: Callable[..., Any] = lambda x: x,
+                   backward: Callable[..., Any] = lambda x: x,
+                   ) -> Self:
         """Bind the value of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.

+ 9 - 7
nicegui/elements/mixins/visibility.py

@@ -11,14 +11,15 @@ if TYPE_CHECKING:
 class Visibility:
     visible = BindableProperty(on_change=lambda sender, visible: sender.on_visibility_change(visible))
 
-    def __init__(self, **kwargs) -> None:
+    def __init__(self, **kwargs: Any) -> None:
         super().__init__(**kwargs)
         self.visible = True
 
     def bind_visibility_to(self,
                            target_object: Any,
                            target_name: str = 'visible',
-                           forward: Callable = lambda x: x) -> Self:
+                           forward: Callable[..., Any] = lambda x: x,
+                           ) -> Self:
         """Bind the visibility of this element to the target object's target_name property.
 
         The binding works one way only, from this element to the target.
@@ -33,7 +34,7 @@ class Visibility:
     def bind_visibility_from(self,
                              target_object: Any,
                              target_name: str = 'visible',
-                             backward: Callable = lambda x: x, *,
+                             backward: Callable[..., Any] = lambda x: x, *,
                              value: Any = None) -> Self:
         """Bind the visibility of this element from the target object's target_name property.
 
@@ -52,9 +53,10 @@ class Visibility:
     def bind_visibility(self,
                         target_object: Any,
                         target_name: str = 'visible', *,
-                        forward: Callable = lambda x: x,
-                        backward: Callable = lambda x: x,
-                        value: Any = None) -> Self:
+                        forward: Callable[..., Any] = lambda x: x,
+                        backward: Callable[..., Any] = lambda x: x,
+                        value: Any = None,
+                        ) -> Self:
         """Bind the visibility of this element to the target object's target_name property.
 
         The binding works both ways, from this element to the target and from the target to this element.
@@ -70,7 +72,7 @@ class Visibility:
         bind(self, 'visible', target_object, target_name, forward=forward, backward=backward)
         return self
 
-    def set_visibility(self, visible: str) -> None:
+    def set_visibility(self, visible: bool) -> None:
         """Set the visibility of this element.
 
         :param visible: Whether the element should be visible.

+ 4 - 3
nicegui/elements/number.py

@@ -17,8 +17,9 @@ class Number(ValueElement, DisableableElement):
                  prefix: Optional[str] = None,
                  suffix: Optional[str] = None,
                  format: Optional[str] = None,
-                 on_change: Optional[Callable] = None,
-                 validation: Dict[str, Callable] = {}) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 validation: Dict[str, Callable[..., bool]] = {},
+                 ) -> None:
         """Number Input
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
@@ -35,7 +36,7 @@ class Number(ValueElement, DisableableElement):
         :param prefix: a prefix to prepend to the displayed value
         :param suffix: a suffix to append to the displayed value
         :param format: a string like "%.2f" to format the displayed value
-        :param on_change: callback to execute when the input is confirmed by leaving the focus
+        :param on_change: callback to execute when the value changes
         :param validation: dictionary of validation rules, e.g. ``{'Too small!': lambda value: value < 3}``
         """
         self.format = format

+ 2 - 1
nicegui/elements/pyplot.py

@@ -1,5 +1,6 @@
 import asyncio
 import io
+from typing import Any
 
 import matplotlib.pyplot as plt
 
@@ -9,7 +10,7 @@ from ..element import Element
 
 class Pyplot(Element):
 
-    def __init__(self, *, close: bool = True, **kwargs) -> None:
+    def __init__(self, *, close: bool = True, **kwargs: Any) -> None:
         """Pyplot Context
 
         Create a context to configure a `Matplotlib <https://matplotlib.org/>`_ plot.

+ 5 - 1
nicegui/elements/radio.py

@@ -6,7 +6,11 @@ from .mixins.disableable_element import DisableableElement
 
 class Radio(ChoiceElement, DisableableElement):
 
-    def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None):
+    def __init__(self,
+                 options: Union[List, Dict], *,
+                 value: Any = None,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Radio Selection
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 2 - 3
nicegui/elements/scene.py

@@ -7,7 +7,6 @@ from ..element import Element
 from ..events import SceneClickEventArguments, SceneClickHit, handle_event
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
-from .scene_objects import Scene as SceneObject
 
 register_component('scene', __file__, 'scene.js', [
     'lib/three.min.js',
@@ -58,7 +57,8 @@ class Scene(Element):
                  width: int = 400,
                  height: int = 300,
                  grid: bool = True,
-                 on_click: Optional[Callable] = None) -> None:
+                 on_click: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """3D Scene
 
         Display a 3d scene using `three.js <https://threejs.org/>`_.
@@ -75,7 +75,6 @@ class Scene(Element):
         self._props['width'] = width
         self._props['height'] = height
         self._props['grid'] = grid
-        self._props['key'] = self.id  # HACK: workaround for #600
         self.objects: Dict[str, Object3D] = {}
         self.stack: List[Union[Object3D, SceneObject]] = [SceneObject()]
         self.camera: SceneCamera = SceneCamera()

+ 3 - 2
nicegui/elements/select.py

@@ -15,8 +15,9 @@ class Select(ChoiceElement, DisableableElement):
     def __init__(self, options: Union[List, Dict], *,
                  label: Optional[str] = None,
                  value: Any = None,
-                 on_change: Optional[Callable] = None,
-                 with_input: bool = False) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 with_input: bool = False,
+                 ) -> None:
         """Dropdown Selection
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 3 - 2
nicegui/elements/slider.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,8 @@ class Slider(ValueElement, DisableableElement):
                  max: float,
                  step: float = 1.0,
                  value: Optional[float] = None,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Slider
 
         :param min: lower bound of the slider

+ 3 - 2
nicegui/elements/splitter.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional, Tuple
+from typing import Any, Callable, Optional, Tuple
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
@@ -11,7 +11,8 @@ class Splitter(ValueElement, DisableableElement):
                  reverse: Optional[bool] = False,
                  limits: Optional[Tuple[float, float]] = (0, 100),
                  value: Optional[float] = 50,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Splitter
 
         The `ui.splitter` element divides the screen space into resizable sections, 

+ 2 - 2
nicegui/elements/switch.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.text_element import TextElement
@@ -7,7 +7,7 @@ from .mixins.value_element import ValueElement
 
 class Switch(TextElement, ValueElement, DisableableElement):
 
-    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable] = None) -> None:
+    def __init__(self, text: str = '', *, value: bool = False, on_change: Optional[Callable[..., Any]] = None) -> None:
         """Switch
 
         :param text: the label to display next to the switch

+ 2 - 2
nicegui/elements/table.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, List, Optional
+from typing import Any, Callable, Dict, List, Optional
 
 from typing_extensions import Literal
 
@@ -16,7 +16,7 @@ class Table(FilterElement):
                  title: Optional[str] = None,
                  selection: Optional[Literal['single', 'multiple']] = None,
                  pagination: Optional[int] = None,
-                 on_select: Optional[Callable] = None,
+                 on_select: Optional[Callable[..., Any]] = None,
                  ) -> None:
         """Table
 

+ 3 - 2
nicegui/elements/tabs.py

@@ -9,7 +9,8 @@ class Tabs(ValueElement):
 
     def __init__(self, *,
                  value: Any = None,
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Tabs
 
         This element represents `Quasar's QTabs <https://quasar.dev/vue-components/tabs#qtabs-api>`_ component.
@@ -47,7 +48,7 @@ class TabPanels(ValueElement):
     def __init__(self,
                  tabs: Tabs, *,
                  value: Any = None,
-                 on_change: Optional[Callable] = None,
+                 on_change: Optional[Callable[..., Any]] = None,
                  animated: bool = True,
                  ) -> None:
         """Tab Panels

+ 5 - 4
nicegui/elements/textarea.py

@@ -1,4 +1,4 @@
-from typing import Callable, Dict, Optional
+from typing import Any, Callable, Dict, Optional
 
 from .input import Input
 
@@ -9,8 +9,9 @@ class Textarea(Input):
                  label: Optional[str] = None, *,
                  placeholder: Optional[str] = None,
                  value: str = '',
-                 on_change: Optional[Callable] = None,
-                 validation: Dict[str, Callable] = {}) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 validation: Dict[str, Callable[..., bool]] = {},
+                 ) -> None:
         """Textarea
 
         This element is based on Quasar's `QInput <https://quasar.dev/vue-components/input>`_ component.
@@ -19,7 +20,7 @@ class Textarea(Input):
         :param label: displayed name for the textarea
         :param placeholder: text to show if no value is entered
         :param value: the initial value of the field
-        :param on_change: callback to execute when the input is confirmed by leaving the focus
+        :param on_change: callback to execute when the value changes
         :param validation: dictionary of validation rules, e.g. ``{'Too short!': lambda value: len(value) < 3}``
         """
         super().__init__(label, placeholder=placeholder, value=value, on_change=on_change, validation=validation)

+ 4 - 4
nicegui/elements/time.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from .mixins.disableable_element import DisableableElement
 from .mixins.value_element import ValueElement
@@ -7,10 +7,10 @@ from .mixins.value_element import ValueElement
 class Time(ValueElement, DisableableElement):
 
     def __init__(self,
-                 value: Optional[str] = None,
-                 *,
+                 value: Optional[str] = None, *,
                  mask: str = 'HH:mm',
-                 on_change: Optional[Callable] = None) -> None:
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Time Input
 
         This element is based on Quasar's `QTime <https://quasar.dev/vue-components/date>`_ component.

+ 5 - 1
nicegui/elements/toggle.py

@@ -6,7 +6,11 @@ from .mixins.disableable_element import DisableableElement
 
 class Toggle(ChoiceElement, DisableableElement):
 
-    def __init__(self, options: Union[List, Dict], *, value: Any = None, on_change: Optional[Callable] = None) -> None:
+    def __init__(self,
+                 options: Union[List, Dict], *,
+                 value: Any = None,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Toggle
 
         The options can be specified as a list of values, or as a dictionary mapping values to labels.

+ 4 - 3
nicegui/elements/tree.py

@@ -11,9 +11,10 @@ class Tree(Element):
                  node_key: str = 'id',
                  label_key: str = 'label',
                  children_key: str = 'children',
-                 on_select: Optional[Callable] = None,
-                 on_expand: Optional[Callable] = None,
-                 on_tick: Optional[Callable] = None) -> None:
+                 on_select: Optional[Callable[..., Any]] = None,
+                 on_expand: Optional[Callable[..., Any]] = None,
+                 on_tick: Optional[Callable[..., Any]] = None,
+                 ) -> None:
         """Tree
 
         Display hierarchical data using Quasar's `QTree <https://quasar.dev/vue-components/tree>`_ component.

+ 3 - 3
nicegui/elements/upload.py

@@ -1,4 +1,4 @@
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from fastapi import Request, Response
 
@@ -17,8 +17,8 @@ class Upload(DisableableElement):
                  max_file_size: Optional[int] = None,
                  max_total_size: Optional[int] = None,
                  max_files: Optional[int] = None,
-                 on_upload: Optional[Callable] = None,
-                 on_rejected: Optional[Callable] = None,
+                 on_upload: Optional[Callable[..., Any]] = None,
+                 on_rejected: Optional[Callable[..., Any]] = None,
                  label: str = '',
                  auto_upload: bool = False,
                  ) -> None:

+ 4 - 4
nicegui/events.py

@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from inspect import signature
-from typing import TYPE_CHECKING, Any, BinaryIO, Callable, List, Optional, Union
+from typing import TYPE_CHECKING, Any, BinaryIO, Callable, Dict, List, Optional, Union
 
 from . import background_tasks, globals
 from .helpers import KWONLY_SLOTS, is_coroutine
@@ -268,15 +268,15 @@ class KeyEventArguments(EventArguments):
     modifiers: KeyboardModifiers
 
 
-def handle_event(handler: Optional[Callable],
-                 arguments: Union[EventArguments, dict], *,
+def handle_event(handler: Optional[Callable[..., Any]],
+                 arguments: Union[EventArguments, Dict], *,
                  sender: Optional['Element'] = None) -> None:
     try:
         if handler is None:
             return
         no_arguments = not signature(handler).parameters
         sender = arguments.sender if isinstance(arguments, EventArguments) else sender
-        assert sender.parent_slot is not None
+        assert sender is not None and sender.parent_slot is not None
         with sender.parent_slot:
             result = handler() if no_arguments else handler(arguments)
         if is_coroutine(handler):

+ 1 - 1
nicegui/functions/notify.py

@@ -10,7 +10,7 @@ def notify(message: Any, *,
            closeBtn: Union[bool, str] = False,
            type: Optional[Literal['positive', 'negative', 'warning', 'info', 'ongoing']] = None,
            color: Optional[str] = None,
-           **kwargs,
+           **kwargs: Any,
            ) -> None:
     """Notification
 

+ 2 - 2
nicegui/functions/open.py

@@ -1,9 +1,9 @@
-from typing import Callable, Union
+from typing import Any, Callable, Union
 
 from .. import globals
 
 
-def open(target: Union[Callable, str]) -> None:
+def open(target: Union[Callable[..., Any], str]) -> None:
     """Open
 
     Can be used to programmatically trigger redirects for a specific client.

+ 39 - 32
nicegui/functions/refreshable.py

@@ -1,18 +1,43 @@
-from typing import Any, Callable, Dict, List, Tuple
+from dataclasses import dataclass
+from typing import Any, Callable, Dict, List
 
 from typing_extensions import Self
 
 from .. import background_tasks, globals
 from ..dependencies import register_component
 from ..element import Element
-from ..helpers import is_coroutine
+from ..helpers import KWONLY_SLOTS, is_coroutine
 
 register_component('refreshable', __file__, 'refreshable.js')
 
 
+@dataclass(**KWONLY_SLOTS)
+class RefreshableTarget:
+    container: Element
+    instance: Any
+    args: List[Any]
+    kwargs: Dict[str, Any]
+
+    def run(self, func: Callable[..., Any]) -> None:
+        if is_coroutine(func):
+            async def wait_for_result() -> None:
+                with self.container:
+                    if self.instance is None:
+                        await func(*self.args, **self.kwargs)
+                    else:
+                        await func(self.instance, *self.args, **self.kwargs)
+            return wait_for_result()
+        else:
+            with self.container:
+                if self.instance is None:
+                    func(*self.args, **self.kwargs)
+                else:
+                    func(self.instance, *self.args, **self.kwargs)
+
+
 class refreshable:
 
-    def __init__(self, func: Callable) -> None:
+    def __init__(self, func: Callable[..., Any]) -> None:
         """Refreshable UI functions
 
         The `@ui.refreshable` decorator allows you to create functions that have a `refresh` method.
@@ -20,23 +45,25 @@ class refreshable:
         """
         self.func = func
         self.instance = None
-        self.containers: List[Tuple[Element, List[Any], Dict[str, Any]]] = []
+        self.targets: List[RefreshableTarget] = []
 
     def __get__(self, instance, _) -> Self:
         self.instance = instance
         return self
 
-    def __call__(self, *args, **kwargs) -> None:
+    def __call__(self, *args: Any, **kwargs: Any) -> None:
         self.prune()
-        with Element('refreshable') as container:
-            self.containers.append((container, args, kwargs))
-        return self._run_in_container(container, *args, **kwargs)
+        target = RefreshableTarget(container=Element('refreshable'), instance=self.instance, args=args, kwargs=kwargs)
+        self.targets.append(target)
+        return target.run(self.func)
 
     def refresh(self) -> None:
         self.prune()
-        for container, args, kwargs in self.containers:
-            container.clear()
-            result = self._run_in_container(container, *args, **kwargs)
+        for target in self.targets:
+            if target.instance != self.instance:
+                continue
+            target.container.clear()
+            result = target.run(self.func)
             if is_coroutine(self.func):
                 if globals.loop and globals.loop.is_running():
                     background_tasks.create(result)
@@ -44,24 +71,4 @@ class refreshable:
                     globals.app.on_startup(result)
 
     def prune(self) -> None:
-        self.containers = [
-            (container, args, kwargs)
-            for container, args, kwargs in self.containers
-            if container.client.id in globals.clients
-        ]
-
-    def _run_in_container(self, container: Element, *args, **kwargs) -> None:
-        if is_coroutine(self.func):
-            async def wait_for_result() -> None:
-                with container:
-                    if self.instance is None:
-                        await self.func(*args, **kwargs)
-                    else:
-                        await self.func(self.instance, *args, **kwargs)
-            return wait_for_result()
-        else:
-            with container:
-                if self.instance is None:
-                    self.func(*args, **kwargs)
-                else:
-                    self.func(self.instance, *args, **kwargs)
+        self.targets = [target for target in self.targets if target.container.client.id in globals.clients]

+ 10 - 4
nicegui/functions/timer.py

@@ -1,6 +1,6 @@
 import asyncio
 import time
-from typing import Callable
+from typing import Any, Callable
 
 from .. import background_tasks, globals
 from ..binding import BindableProperty
@@ -11,7 +11,12 @@ class Timer:
     active = BindableProperty()
     interval = BindableProperty()
 
-    def __init__(self, interval: float, callback: Callable, *, active: bool = True, once: bool = False) -> None:
+    def __init__(self,
+                 interval: float,
+                 callback: Callable[..., Any], *,
+                 active: bool = True,
+                 once: bool = False,
+                 ) -> None:
         """Timer
 
         One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals,
@@ -78,10 +83,11 @@ class Timer:
             globals.handle_exception(e)
 
     async def _connected(self, timeout: float = 60.0) -> bool:
-        '''Wait for the client connection before the timer callback can be allowed to manipulate the state.
+        """Wait for the client connection before the timer callback can be allowed to manipulate the state.
+
         See https://github.com/zauberzeug/nicegui/issues/206 for details.
         Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
-        '''
+        """
         if self.slot.parent.client.shared:
             return True
         else:

+ 9 - 7
nicegui/globals.py

@@ -3,13 +3,14 @@ import inspect
 import logging
 from contextlib import contextmanager
 from enum import Enum
-from typing import TYPE_CHECKING, Awaitable, Callable, Dict, List, Optional, Union
+from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, List, Optional, Union
 
 from socketio import AsyncServer
 from uvicorn import Server
 
 from . import background_tasks
 from .app import App
+from .language import Language
 
 if TYPE_CHECKING:
     from .client import Client
@@ -36,6 +37,7 @@ title: str
 viewport: str
 favicon: Optional[str]
 dark: Optional[bool]
+language: Language
 binding_refresh_interval: float
 excludes: List[str]
 tailwind: bool
@@ -46,13 +48,13 @@ slot_stacks: Dict[int, List['Slot']] = {}
 clients: Dict[str, 'Client'] = {}
 index_client: 'Client'
 
-page_routes: Dict[Callable, str] = {}
+page_routes: Dict[Callable[..., Any], str] = {}
 
-startup_handlers: List[Union[Callable, Awaitable]] = []
-shutdown_handlers: List[Union[Callable, Awaitable]] = []
-connect_handlers: List[Union[Callable, Awaitable]] = []
-disconnect_handlers: List[Union[Callable, Awaitable]] = []
-exception_handlers: List[Callable] = [log.exception]
+startup_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+shutdown_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+connect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+disconnect_handlers: List[Union[Callable[..., Any], Awaitable]] = []
+exception_handlers: List[Callable[..., Any]] = [log.exception]
 
 
 def get_task_id() -> int:

+ 1 - 1
nicegui/helpers.py

@@ -23,7 +23,7 @@ def is_coroutine(object: Any) -> bool:
     return asyncio.iscoroutinefunction(object)
 
 
-def safe_invoke(func: Union[Callable, Awaitable], client: Optional['Client'] = None) -> None:
+def safe_invoke(func: Union[Callable[..., Any], Awaitable], client: Optional['Client'] = None) -> None:
     try:
         if isinstance(func, Awaitable):
             async def func_with_client():

+ 3 - 0
nicegui/json/orjson_wrapper.py

@@ -1,3 +1,4 @@
+from decimal import Decimal
 from typing import Any, Optional, Tuple
 
 import numpy as np
@@ -42,6 +43,8 @@ def _orjson_converter(obj):
     """Custom serializer/converter, e.g. for numpy object arrays."""
     if isinstance(obj, np.ndarray) and obj.dtype == np.object_:
         return obj.tolist()
+    if isinstance(obj, Decimal):
+        return float(obj)
 
 
 class NiceGUIJSONResponse(Response):

+ 66 - 0
nicegui/language.py

@@ -0,0 +1,66 @@
+from typing_extensions import Literal
+
+Language = Literal[
+    'ar',
+    'ar-TN',
+    'az-Latn',
+    'bg',
+    'bn',
+    'ca',
+    'cs',
+    'da',
+    'de',
+    'el',
+    'en-GB',
+    'en-US',
+    'eo',
+    'es',
+    'et',
+    'eu',
+    'fa',
+    'fa-IR',
+    'fi',
+    'fr',
+    'gn',
+    'he',
+    'hr',
+    'hu',
+    'id',
+    'is',
+    'it',
+    'ja',
+    'kk',
+    'km',
+    'ko-KR',
+    'kur-CKB',
+    'lt',
+    'lu',
+    'lv',
+    'ml',
+    'mm',
+    'ms',
+    'my',
+    'nb-NO',
+    'nl',
+    'pl',
+    'pt',
+    'pt-BR',
+    'ro',
+    'ru',
+    'sk',
+    'sl',
+    'sm',
+    'sr',
+    'sr-CYR',
+    'sv',
+    'ta',
+    'th',
+    'tr',
+    'ug',
+    'uk',
+    'uz-Cyrl',
+    'uz-Latn',
+    'vi',
+    'zh-CN',
+    'zh-TW',
+]

+ 7 - 3
nicegui/nicegui.py

@@ -83,10 +83,14 @@ def handle_startup(with_welcome_message: bool = True) -> None:
 def print_welcome_message():
     host = os.environ['NICEGUI_HOST']
     port = os.environ['NICEGUI_PORT']
-    ips = ['localhost']
+    ips = set()
     if host == '0.0.0.0':
-        ips += set(info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None))
-    addresses = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ips]
+        try:
+            ips.update(set(info[4][0] for info in socket.getaddrinfo(socket.gethostname(), None) if len(info[4]) == 2))
+        except Exception:
+            pass  # NOTE: if we can't get the host's IP, we'll just use localhost
+    ips.discard('127.0.0.1')
+    addresses = [(f'http://{ip}:{port}' if port != '80' else f'http://{ip}') for ip in ['localhost'] + sorted(ips)]
     if len(addresses) >= 2:
         addresses[-1] = 'and ' + addresses[-1]
     print(f'NiceGUI ready to go on {", ".join(addresses)}')

+ 10 - 3
nicegui/page.py

@@ -1,13 +1,14 @@
 import asyncio
 import inspect
 import time
-from typing import Callable, Optional
+from typing import Any, Callable, Optional
 
 from fastapi import Request, Response
 
 from . import background_tasks, globals
 from .client import Client
 from .favicon import create_favicon_route
+from .language import Language
 
 
 class page:
@@ -18,8 +19,9 @@ class page:
                  viewport: Optional[str] = None,
                  favicon: Optional[str] = None,
                  dark: Optional[bool] = ...,
+                 language: Language = ...,
                  response_timeout: float = 3.0,
-                 **kwargs,
+                 **kwargs: Any,
                  ) -> None:
         """Page
 
@@ -33,6 +35,7 @@ class page:
         :param viewport: optional viewport meta tag content
         :param favicon: optional relative filepath or absolute URL to a favicon (default: `None`, NiceGUI icon will be used)
         :param dark: whether to use Quasar's dark mode (defaults to `dark` argument of `run` command)
+        :param language: language of the page (defaults to `language` argument of `run` command)
         :param response_timeout: maximum time for the decorated function to build the page (default: 3.0)
         :param kwargs: additional keyword arguments passed to FastAPI's @app.get method
         """
@@ -41,6 +44,7 @@ class page:
         self.viewport = viewport
         self.favicon = favicon
         self.dark = dark
+        self.language = language
         self.response_timeout = response_timeout
         self.kwargs = kwargs
 
@@ -55,7 +59,10 @@ class page:
     def resolve_dark(self) -> Optional[bool]:
         return self.dark if self.dark is not ... else globals.dark
 
-    def __call__(self, func: Callable) -> Callable:
+    def resolve_language(self) -> Optional[str]:
+        return self.language if self.language is not ... else globals.language
+
+    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
         globals.app.remove_route(self.path)  # NOTE make sure only the latest route definition is used
         parameters_of_decorated_func = list(inspect.signature(func).parameters.keys())
 

+ 6 - 2
nicegui/run.py

@@ -2,7 +2,7 @@ import logging
 import multiprocessing
 import os
 import sys
-from typing import List, Optional, Tuple
+from typing import Any, List, Optional, Tuple
 
 import __main__
 import uvicorn
@@ -10,6 +10,7 @@ from uvicorn.main import STARTUP_FAILURE
 from uvicorn.supervisors import ChangeReload, Multiprocess
 
 from . import globals, helpers, native_mode
+from .language import Language
 
 
 def run(*,
@@ -19,6 +20,7 @@ def run(*,
         viewport: str = 'width=device-width, initial-scale=1',
         favicon: Optional[str] = None,
         dark: Optional[bool] = False,
+        language: Language = 'en-US',
         binding_refresh_interval: float = 0.1,
         show: bool = True,
         native: bool = False,
@@ -31,7 +33,7 @@ def run(*,
         uvicorn_reload_excludes: str = '.*, .py[cod], .sw.*, ~*',
         exclude: str = '',
         tailwind: bool = True,
-        **kwargs,
+        **kwargs: Any,
         ) -> None:
     '''ui.run
 
@@ -43,6 +45,7 @@ def run(*,
     :param viewport: page meta viewport content (default: `'width=device-width, initial-scale=1'`, can be overwritten per page)
     :param favicon: relative filepath, absolute URL to a favicon (default: `None`, NiceGUI icon will be used) or emoji (e.g. `'🚀'`, works for most browsers)
     :param dark: whether to use Quasar's dark mode (default: `False`, use `None` for "auto" mode)
+    :param language: language for Quasar elements (default: `'en-US'`)
     :param binding_refresh_interval: time between binding updates (default: `0.1` seconds, bigger is more CPU friendly)
     :param show: automatically open the UI in a browser tab (default: `True`)
     :param native: open the UI in a native window of size 800x600 (default: `False`, deactivates `show`, automatically finds an open port)
@@ -64,6 +67,7 @@ def run(*,
     globals.viewport = viewport
     globals.favicon = favicon
     globals.dark = dark
+    globals.language = language
     globals.binding_refresh_interval = binding_refresh_interval
     globals.excludes = [e.strip() for e in exclude.split(',')]
     globals.tailwind = tailwind

+ 3 - 0
nicegui/run_with.py

@@ -3,6 +3,7 @@ from typing import Optional
 from fastapi import FastAPI
 
 from nicegui import globals
+from nicegui.language import Language
 from nicegui.nicegui import handle_shutdown, handle_startup
 
 
@@ -12,6 +13,7 @@ def run_with(
     viewport: str = 'width=device-width, initial-scale=1',
     favicon: Optional[str] = None,
     dark: Optional[bool] = False,
+    language: Language = 'en-US',
     binding_refresh_interval: float = 0.1,
     exclude: str = '',
     mount_path: str = '/',
@@ -21,6 +23,7 @@ def run_with(
     globals.viewport = viewport
     globals.favicon = favicon
     globals.dark = dark
+    globals.language = language
     globals.binding_refresh_interval = binding_refresh_interval
     globals.excludes = [e.strip() for e in exclude.split(',')]
     globals.tailwind = True

+ 4 - 1
nicegui/slot.py

@@ -1,4 +1,4 @@
-from typing import TYPE_CHECKING, List, Optional
+from typing import TYPE_CHECKING, Iterator, List, Optional
 
 from typing_extensions import Self
 
@@ -23,3 +23,6 @@ class Slot:
     def __exit__(self, *_) -> None:
         globals.get_slot_stack().pop()
         globals.prune_slot_stack()
+
+    def __iter__(self) -> Iterator['Element']:
+        return iter(self.children)

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.ar-TN.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.ar.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.az-Latn.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.bg.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.bn.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.ca.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.cs.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.da.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.de.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.el.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.en-GB.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.en-US.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.eo.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.es.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.et.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.eu.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.fa-IR.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.fa.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.fi.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.fr.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.gn.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.he.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.hr.umd.prod.js


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 5 - 0
nicegui/static/quasar.hu.umd.prod.js


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio