Преглед на файлове

Merge commit '88ff37f57b6abfece6a70d6bce4669ad390207f3' into changeable_native_window

Rodja Trappe преди 2 години
родител
ревизия
d081cc8061

+ 10 - 10
CITATION.cff

@@ -1,14 +1,14 @@
 cff-version: 1.2.0
 cff-version: 1.2.0
 message: If you use this software, please cite it as below.
 message: If you use this software, please cite it as below.
 authors:
 authors:
-  - family-names: Schindler
-    given-names: Falko
-    orcid: https://orcid.org/0009-0003-5359-835X
-  - family-names: Trappe
-    given-names: Rodja
-    orcid: https://orcid.org/0009-0009-4735-6227
-title: "NiceGUI: Web-based user interfaces with Python. The nice way."
-version: v1.2.13
-date-released: "2023-05-05"
+- family-names: Schindler
+  given-names: Falko
+  orcid: https://orcid.org/0009-0003-5359-835X
+- family-names: Trappe
+  given-names: Rodja
+  orcid: https://orcid.org/0009-0009-4735-6227
+title: 'NiceGUI: Web-based user interfaces with Python. The nice way.'
+version: v1.2.14
+date-released: '2023-05-14'
 url: https://github.com/zauberzeug/nicegui
 url: https://github.com/zauberzeug/nicegui
-doi: 10.5281/zenodo.7901074
+doi: 10.5281/zenodo.7933863

+ 2 - 2
examples/local_file_picker/local_file_picker.py

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

+ 4 - 3
fetch_tailwind.py

@@ -2,6 +2,7 @@
 import re
 import re
 from dataclasses import dataclass, field
 from dataclasses import dataclass, field
 from pathlib import Path
 from pathlib import Path
+from typing import List
 
 
 import requests
 import requests
 from bs4 import BeautifulSoup
 from bs4 import BeautifulSoup
@@ -12,8 +13,8 @@ from secure import SecurePath
 class Property:
 class Property:
     title: str
     title: str
     description: 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)
     common_prefix: str = field(init=False)
 
 
     def __post_init__(self) -> None:
     def __post_init__(self) -> None:
@@ -48,7 +49,7 @@ class Property:
         return '_'.join(word.lower() for word in re.sub(r'[-/ &]', ' ', self.title).split())
         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:
 def get_soup(url: str) -> BeautifulSoup:

+ 17 - 14
main.py

@@ -10,10 +10,10 @@ if True:
 
 
 import os
 import os
 from pathlib import Path
 from pathlib import Path
-from typing import Optional
+from typing import Awaitable, Callable, Optional
 
 
 from fastapi import Request
 from fastapi import Request
-from fastapi.responses import FileResponse, RedirectResponse
+from fastapi.responses import FileResponse, RedirectResponse, Response
 from starlette.middleware.sessions import SessionMiddleware
 from starlette.middleware.sessions import SessionMiddleware
 
 
 import prometheus
 import prometheus
@@ -36,17 +36,18 @@ app.add_static_files('/fonts', str(Path(__file__).parent / 'website' / 'fonts'))
 
 
 
 
 @app.get('/logo.png')
 @app.get('/logo.png')
-def logo():
+def logo() -> FileResponse:
     return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
     return FileResponse(svg.PATH / 'logo.png', media_type='image/png')
 
 
 
 
 @app.get('/logo_square.png')
 @app.get('/logo_square.png')
-def logo():
+def logo_square() -> FileResponse:
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
     return FileResponse(svg.PATH / 'logo_square.png', media_type='image/png')
 
 
 
 
 @app.middleware('http')
 @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':
     if request.url.path == '/reference':
         return RedirectResponse('/documentation')
         return RedirectResponse('/documentation')
     return await call_next(request)
     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') \
             .classes('items-center duration-200 p-0 px-4 no-wrap') \
             .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
             .style('box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1)'):
         if menu:
         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'):
         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.face().classes('w-8 stroke-white stroke-2')
             svg.word().classes('w-24')
             svg.word().classes('w-24')
         with ui.row().classes('max-lg:hidden'):
         with ui.row().classes('max-lg:hidden'):
             for title, target in menu_items.items():
             for title, target in menu_items.items():
                 ui.link(title, target).classes(replace='text-lg text-white')
                 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')
             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')
             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.row().classes('lg:hidden'):
             with ui.button().props('flat color=white icon=more_vert round'):
             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'):
                 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('/')
 @ui.page('/')
-async def index_page(client: Client):
+async def index_page(client: Client) -> None:
     client.content.classes('p-0 gap-0')
     client.content.classes('p-0 gap-0')
     add_head_html()
     add_head_html()
     add_header()
     add_header()
@@ -316,7 +318,7 @@ async def index_page(client: Client):
 
 
 
 
 @ui.page('/documentation')
 @ui.page('/documentation')
-def documentation_page():
+def documentation_page() -> None:
     add_head_html()
     add_head_html()
     menu = side_menu()
     menu = side_menu()
     add_header(menu)
     add_header(menu)
@@ -331,7 +333,7 @@ def documentation_page():
 
 
 
 
 @ui.page('/documentation/{name}')
 @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):
     if not hasattr(ui, name):
         name = name.replace('_', '')  # NOTE: "AG Grid" leads to anchor name "ag_grid", but class is `ui.aggrid`
         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')
     module = importlib.import_module(f'website.more_documentation.{name}_documentation')
@@ -359,6 +361,7 @@ def documentation_page_more(name: str):
                 ui.markdown('**Reference**').classes('mt-4')
                 ui.markdown('**Reference**').classes('mt-4')
             ui.markdown('## Reference').classes('mt-16')
             ui.markdown('## Reference').classes('mt-16')
             generate_class_doc(api)
             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')
 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 .client import Client
 from .nicegui import app
 from .nicegui import app
 from .tailwind import Tailwind
 from .tailwind import Tailwind
+
+__all__ = [
+    'app',
+    'Client',
+    'elements',
+    'globals',
+    'Tailwind',
+    'ui',
+    '__version__',
+]

+ 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 asyncio
 import sys
 import sys
 from typing import Awaitable, Dict, Set, TypeVar
 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]':
 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.
     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.
     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.
     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 = globals.loop.create_task(coroutine, name=name) if name_supported else globals.loop.create_task(coroutine)
     task.add_done_callback(_handle_task_result)
     task.add_done_callback(_handle_task_result)
     running_tasks.add(task)
     running_tasks.add(task)
@@ -28,11 +28,11 @@ def create(coroutine: Awaitable[T], *, name: str = 'unnamed task') -> 'asyncio.T
     return task
     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 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:
     if name in lazy_tasks_running:
         lazy_tasks_waiting[name] = coroutine
         lazy_tasks_waiting[name] = coroutine
         return
         return

+ 1 - 1
nicegui/client.py

@@ -112,7 +112,7 @@ class Client:
         """Execute JavaScript on the client.
         """Execute JavaScript on the client.
 
 
         The client connection must be established before this method is called.
         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.
         If respond is True, the javascript code must return a string.
         """
         """
         request_id = str(uuid.uuid4())
         request_id = str(uuid.uuid4())

+ 12 - 5
nicegui/elements/chart.js

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

+ 1 - 1
nicegui/elements/mixins/visibility.py

@@ -70,7 +70,7 @@ class Visibility:
         bind(self, 'visible', target_object, target_name, forward=forward, backward=backward)
         bind(self, 'visible', target_object, target_name, forward=forward, backward=backward)
         return self
         return self
 
 
-    def set_visibility(self, visible: str) -> None:
+    def set_visibility(self, visible: bool) -> None:
         """Set the visibility of this element.
         """Set the visibility of this element.
 
 
         :param visible: Whether the element should be visible.
         :param visible: Whether the element should be visible.

+ 0 - 1
nicegui/elements/scene.py

@@ -7,7 +7,6 @@ from ..element import Element
 from ..events import SceneClickEventArguments, SceneClickHit, handle_event
 from ..events import SceneClickEventArguments, SceneClickHit, handle_event
 from ..helpers import KWONLY_SLOTS
 from ..helpers import KWONLY_SLOTS
 from .scene_object3d import Object3D
 from .scene_object3d import Object3D
-from .scene_objects import Scene as SceneObject
 
 
 register_component('scene', __file__, 'scene.js', [
 register_component('scene', __file__, 'scene.js', [
     'lib/three.min.js',
     'lib/three.min.js',

+ 3 - 3
nicegui/events.py

@@ -1,6 +1,6 @@
 from dataclasses import dataclass
 from dataclasses import dataclass
 from inspect import signature
 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 . import background_tasks, globals
 from .helpers import KWONLY_SLOTS, is_coroutine
 from .helpers import KWONLY_SLOTS, is_coroutine
@@ -269,14 +269,14 @@ class KeyEventArguments(EventArguments):
 
 
 
 
 def handle_event(handler: Optional[Callable],
 def handle_event(handler: Optional[Callable],
-                 arguments: Union[EventArguments, dict], *,
+                 arguments: Union[EventArguments, Dict], *,
                  sender: Optional['Element'] = None) -> None:
                  sender: Optional['Element'] = None) -> None:
     try:
     try:
         if handler is None:
         if handler is None:
             return
             return
         no_arguments = not signature(handler).parameters
         no_arguments = not signature(handler).parameters
         sender = arguments.sender if isinstance(arguments, EventArguments) else sender
         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:
         with sender.parent_slot:
             result = handler() if no_arguments else handler(arguments)
             result = handler() if no_arguments else handler(arguments)
         if is_coroutine(handler):
         if is_coroutine(handler):

+ 1 - 1
nicegui/templates/index.html

@@ -76,7 +76,7 @@
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           let event_name = 'on' + event.type[0].toLocaleUpperCase() + event.type.substring(1);
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
           event.specials.forEach(s => event_name += s[0].toLocaleUpperCase() + s.substring(1));
           let handler = (e) => {
           let handler = (e) => {
-            const all = typeof e !== 'object' || !event.args;
+            const all = (typeof e !== 'object' || e === null) || !event.args;
             const args = all ? e : Object.fromEntries(event.args.map(a => [a, e[a]]));
             const args = all ? e : Object.fromEntries(event.args.map(a => [a, e[a]]));
             const emitter = () => window.socket.emit("event", {id: element.id, listener_id: event.listener_id, args});
             const emitter = () => window.socket.emit("event", {id: element.id, listener_id: event.listener_id, args});
             throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);
             throttle(emitter, event.throttle, event.leading_events, event.trailing_events, event.listener_id);

+ 86 - 0
nicegui/ui.py

@@ -1,5 +1,90 @@
 import os
 import os
 
 
+__all__ = [
+    'deprecated',
+    'element',
+    'aggrid',
+    'audio',
+    'avatar',
+    'badge',
+    'button',
+    'card',
+    'card_actions',
+    'card_section',
+    'chart',
+    'chat_message',
+    'checkbox',
+    'color_input',
+    'color_picker',
+    'colors',
+    'column',
+    'dark_mode',
+    'date',
+    'dialog',
+    'expansion',
+    'grid',
+    'html',
+    'icon',
+    'image',
+    'input',
+    'interactive_image',
+    'joystick',
+    'keyboard',
+    'knob',
+    'label',
+    'link',
+    'link_target',
+    'log',
+    'markdown',
+    'menu',
+    'menu_item',
+    'mermaid',
+    'number',
+    'plotly',
+    'circular_progress',
+    'linear_progress',
+    'query',
+    'radio',
+    'row',
+    'scene',
+    'select',
+    'separator',
+    'slider',
+    'spinner',
+    'splitter',
+    'switch',
+    'table',
+    'tab',
+    'tab_panel',
+    'tab_panels',
+    'tabs',
+    'textarea',
+    'time',
+    'toggle',
+    'tooltip',
+    'tree',
+    'upload',
+    'video',
+    'download',
+    'add_body_html',
+    'add_head_html',
+    'run_javascript',
+    'notify',
+    'open',
+    'refreshable',
+    'timer',
+    'update',
+    'page',
+    'drawer',
+    'footer',
+    'header',
+    'left_drawer',
+    'page_sticky',
+    'right_drawer',
+    'run',
+    'run_with',
+]
+
 from .deprecation import deprecated
 from .deprecation import deprecated
 from .element import Element as element
 from .element import Element as element
 from .elements.aggrid import AgGrid as aggrid
 from .elements.aggrid import AgGrid as aggrid
@@ -86,3 +171,4 @@ if os.environ.get('MATPLOTLIB', 'true').lower() == 'true':
     from .elements.line_plot import LinePlot as line_plot
     from .elements.line_plot import LinePlot as line_plot
     from .elements.pyplot import Pyplot as pyplot
     from .elements.pyplot import Pyplot as pyplot
     plot = deprecated(pyplot, 'ui.plot', 'ui.pyplot', 317)
     plot = deprecated(pyplot, 'ui.plot', 'ui.pyplot', 317)
+    __all__.extend(['line_plot', 'pyplot', 'plot'])

+ 22 - 0
tests/test_chart.py

@@ -109,3 +109,25 @@ def test_replace_chart(screen: Screen):
     screen.click('Replace')
     screen.click('Replace')
     screen.should_contain('B')
     screen.should_contain('B')
     screen.should_not_contain('A')
     screen.should_not_contain('A')
+
+
+def test_stock_chart(screen: Screen):
+    """https://github.com/zauberzeug/nicegui/discussions/948"""
+    chart = ui.chart({'legend': {'enabled': True}, 'series': []}, type='stockChart', extras=['stock'])
+    ui.button('update', on_click=lambda: (
+        chart.options['series'].extend([{'name': 'alice'}, {'name': 'bob'}]),
+        chart.update(),
+    ))
+    ui.button('clear', on_click=lambda: (
+        chart.options['series'].clear(),
+        chart.update(),
+    ))
+
+    screen.open('/')
+    screen.click('update')
+    screen.should_contain('alice')
+    screen.should_contain('bob')
+    screen.click('clear')
+    screen.wait(0.5)
+    screen.should_not_contain('alice')
+    screen.should_not_contain('bob')

+ 10 - 0
tests/test_input.py

@@ -108,3 +108,13 @@ def test_autocompletion(screen: Screen):
     element.send_keys(Keys.TAB)
     element.send_keys(Keys.TAB)
     screen.wait(0.5)
     screen.wait(0.5)
     assert element.get_attribute('value') == 'fx'
     assert element.get_attribute('value') == 'fx'
+
+
+def test_clearable_input(screen: Screen):
+    input = ui.input(value='foo').props('clearable')
+    ui.label().bind_text_from(input, 'value', lambda value: f'value: {value}')
+
+    screen.open('/')
+    screen.should_contain('value: foo')
+    screen.click('cancel')
+    screen.should_contain('value: None')

+ 10 - 0
tests/test_number.py

@@ -29,3 +29,13 @@ def test_max_value(screen: Screen):
     element.send_keys('6')
     element.send_keys('6')
     screen.click('Button')
     screen.click('Button')
     screen.should_contain_input('10')
     screen.should_contain_input('10')
+
+
+def test_clearable_number(screen: Screen):
+    number = ui.number(value=42).props('clearable')
+    ui.label().bind_text_from(number, 'value', lambda value: f'value: {value}')
+
+    screen.open('/')
+    screen.should_contain('value: 42')
+    screen.click('cancel')
+    screen.should_contain('value: None')

+ 2 - 2
website/example_card.py

@@ -3,7 +3,7 @@ from nicegui import ui
 from . import svg
 from . import svg
 
 
 
 
-def create():
+def create() -> None:
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
@@ -27,7 +27,7 @@ def create():
                 ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
                 ui.radio(['A', 'B', 'C'], value='A', on_change=lambda e: output.set_text(e.value)).props('inline')
 
 
 
 
-def create_narrow():
+def create_narrow() -> None:
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
     with ui.row().style('filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1))'):
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
         with ui.card().style(r'clip-path: polygon(0 0, 100% 0, 100% 90%, 0 100%)') \
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):
                 .classes('pb-16 no-shadow'), ui.row().classes('no-wrap'):

+ 12 - 0
website/more_documentation/button_documentation.py

@@ -8,6 +8,18 @@ def main_demo() -> None:
 
 
 
 
 def more() -> None:
 def more() -> None:
+    @text_demo('Icons', '''
+        You can also add an icon to a button.
+    ''')
+    async def icons() -> None:
+        with ui.row():
+            ui.button('demo').props('icon=history')
+            ui.button().props('icon=thumb_up')
+            with ui.button():
+                ui.label('sub-elements')
+                ui.image('https://picsum.photos/id/377/640/360') \
+                    .classes('rounded-full w-16 h-16 ml-4')
+
     @text_demo('Await button click', '''
     @text_demo('Await button click', '''
         Sometimes it is convenient to wait for a button click before continuing the execution.
         Sometimes it is convenient to wait for a button click before continuing the execution.
     ''')
     ''')

+ 15 - 0
website/more_documentation/column_documentation.py

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

+ 21 - 0
website/more_documentation/label_documentation.py

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

+ 16 - 1
website/more_documentation/notify_documentation.py

@@ -4,7 +4,7 @@ from ..documentation_tools import text_demo
 
 
 
 
 def main_demo() -> None:
 def main_demo() -> None:
-    ui.button('Say hi!', on_click=lambda: ui.notify('Hi!', close_button='OK'))
+    ui.button('Say hi!', on_click=lambda: ui.notify('Hi!', closeBtn='OK'))
 
 
 
 
 def more() -> None:
 def more() -> None:
@@ -15,3 +15,18 @@ def more() -> None:
         ui.button('negative', on_click=lambda: ui.notify('error', type='negative'))
         ui.button('negative', on_click=lambda: ui.notify('error', type='negative'))
         ui.button('positive', on_click=lambda: ui.notify('success', type='positive'))
         ui.button('positive', on_click=lambda: ui.notify('success', type='positive'))
         ui.button('warning', on_click=lambda: ui.notify('warning', type='warning'))
         ui.button('warning', on_click=lambda: ui.notify('warning', type='warning'))
+
+    @text_demo('Multiline Notifications', '''
+        To allow a notification text to span multiple lines, it is sufficient to pass the `mutliLine` keyword with `True`.
+        If manual newline breaks are required (e.g. `\n`), you need to define a CSS style and pass it to the notification as shown in the example.
+    ''')
+    def multiline():
+        ui.html('<style>.multi-line-notification { white-space: pre-line; }</style>')
+        ui.button('show', on_click=lambda: ui.notify(
+            'Lorem ipsum dolor sit amet, consectetur adipisicing elit. \n'
+            'Hic quisquam non ad sit assumenda consequuntur esse inventore officia. \n'
+            'Corrupti reiciendis impedit vel, '
+            'fugit odit quisquam quae porro exercitationem eveniet quasi.',
+            multiLine=True,
+            classes='multi-line-notification',
+        ))

+ 21 - 1
website/more_documentation/table_documentation.py

@@ -1,3 +1,5 @@
+from typing import Dict
+
 from nicegui import ui
 from nicegui import ui
 
 
 from ..documentation_tools import text_demo
 from ..documentation_tools import text_demo
@@ -75,7 +77,7 @@ def more() -> None:
         visible_columns = {column['name'] for column in columns}
         visible_columns = {column['name'] for column in columns}
         table = ui.table(columns=columns, rows=rows, row_key='name')
         table = ui.table(columns=columns, rows=rows, row_key='name')
 
 
-        def toggle(column: dict, visible: bool) -> None:
+        def toggle(column: Dict, visible: bool) -> None:
             if visible:
             if visible:
                 visible_columns.add(column['name'])
                 visible_columns.add(column['name'])
             else:
             else:
@@ -140,3 +142,21 @@ def more() -> None:
             columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
             columns=[{'name': col, 'label': col, 'field': col} for col in df.columns],
             rows=df.to_dict('records'),
             rows=df.to_dict('records'),
         )
         )
+
+    @text_demo('Adding rows', '''
+        It's simple to add new rows with the `add_rows(dict)` method.
+    ''')
+    def adding_rows():
+        import os
+        import random
+
+        def add():
+            item = os.urandom(10 // 2).hex()
+            table.add_rows({'id': item, 'count': random.randint(0, 100)})
+
+        ui.button('add', on_click=add)
+        columns = [
+            {'name': 'id', 'label': 'ID', 'field': 'id'},
+            {'name': 'count', 'label': 'Count', 'field': 'count'},
+        ]
+        table = ui.table(columns=columns, rows=[], row_key='id').classes('w-full')

+ 3 - 0
website/static/reddit.svg

@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="3 3 14 14" width="24" height="24">
+    <path fill="#FFF" d="M16.67,10A1.46,1.46,0,0,0,14.2,9a7.12,7.12,0,0,0-3.85-1.23L11,4.65,13.14,5.1a1,1,0,1,0,.13-0.61L10.82,4a0.31,0.31,0,0,0-.37.24L9.71,7.71a7.14,7.14,0,0,0-3.9,1.23A1.46,1.46,0,1,0,4.2,11.33a2.87,2.87,0,0,0,0,.44c0,2.24,2.61,4.06,5.83,4.06s5.83-1.82,5.83-4.06a2.87,2.87,0,0,0,0-.44A1.46,1.46,0,0,0,16.67,10Zm-10,1a1,1,0,1,1,1,1A1,1,0,0,1,6.67,11Zm5.81,2.75a3.84,3.84,0,0,1-2.47.77,3.84,3.84,0,0,1-2.47-.77,0.27,0.27,0,0,1,.38-0.38A3.27,3.27,0,0,0,10,14a3.28,3.28,0,0,0,2.09-.61A0.27,0.27,0,1,1,12.48,13.79Zm-0.18-1.71a1,1,0,1,1,1-1A1,1,0,0,1,12.29,12.08Z"></path>
+</svg>

+ 5 - 0
website/svg.py

@@ -19,5 +19,10 @@ def word() -> ui.html:
 def github() -> ui.html:
 def github() -> ui.html:
     return ui.html((PATH / 'github.svg').read_text())
     return ui.html((PATH / 'github.svg').read_text())
 
 
+
 def discord() -> ui.html:
 def discord() -> ui.html:
     return ui.html((PATH / 'discord.svg').read_text())
     return ui.html((PATH / 'discord.svg').read_text())
+
+
+def reddit() -> ui.html:
+    return ui.html((PATH / 'reddit.svg').read_text())