This commit is contained in:
2025-09-23 04:16:05 +02:00
parent 244bfa11cb
commit 01d9dc9fa2
16 changed files with 2604 additions and 1 deletions

View File

@@ -0,0 +1,3 @@
"""NiceGUI Extensions - Extensions for NiceGUI with proper typing support"""
__version__ = "0.1.0"

View File

@@ -0,0 +1,10 @@
from .base import AsyncElement
from .elements import (AsyncColumn, AsyncRow, AsyncCard, AsyncScrollArea)
__all__ = [
'AsyncElement',
'AsyncColumn',
'AsyncRow',
'AsyncCard',
'AsyncScrollArea'
]

View File

@@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from typing import Type, Any, TypeVar, Generic
from nicegui import ui
# Type variable for the wrapped element type
T = TypeVar('T', bound=ui.element)
class AsyncElement(ABC, Generic[T]):
"""Base class for UI elements with async initialization that behaves like a NiceGUI element"""
def __init__(self, element_type: Type[T] = ui.column, *element_args, **element_kwargs) -> None:
# Create the NiceGUI element
self._element: T = element_type(*element_args, **element_kwargs)
@abstractmethod
async def build(self, *args, **kwargs) -> None:
"""Build/setup the element - must be implemented by subclasses"""
...
@classmethod
async def create(cls, element_type: Type[T] = ui.column, *args, **kwargs) -> T:
"""Factory method to create and build an element instance, returns the NiceGUI element"""
# Separate element constructor args from build args
element_args = kwargs.pop('element_args', ())
element_kwargs = kwargs.pop('element_kwargs', {})
# Create and build the instance
instance = cls(element_type, *element_args, **element_kwargs)
await instance.build(*args, **kwargs)
# Add a reference to the async instance on the element for potential later access
instance._element._async_instance = instance # pyright: ignore[reportAttributeAccessIssue]
return instance._element # Return the NiceGUI element with proper typing
def __getattr__(self, name: str) -> Any:
"""Delegate any missing attribute access to the wrapped element"""
return getattr(self._element, name)
def __enter__(self):
"""Support context manager protocol"""
return self._element.__enter__()
def __exit__(self, *args):
"""Support context manager protocol"""
return self._element.__exit__(*args)

View File

@@ -0,0 +1,79 @@
from abc import ABC, abstractmethod
from typing import Self
from nicegui import ui
class AsyncColumn(ui.column, ABC):
"""Async column that inherits from ui.column for perfect typing"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@abstractmethod
async def build(self, *args, **kwargs) -> None:
"""Build/setup the element - must be implemented by subclasses"""
...
@classmethod
async def create(cls, *args, **kwargs) -> Self:
"""Factory method to create and build a column instance"""
instance = cls()
await instance.build(*args, **kwargs)
return instance
class AsyncRow(ui.row, ABC):
"""Async row that inherits from ui.row for perfect typing"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@abstractmethod
async def build(self, *args, **kwargs) -> None:
"""Build/setup the element - must be implemented by subclasses"""
...
@classmethod
async def create(cls, *args, **kwargs) -> Self:
"""Factory method to create and build a column instance"""
instance = cls()
await instance.build(*args, **kwargs)
return instance
class AsyncCard(ui.card, ABC):
"""Async card that inherits from ui.card for perfect typing"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@abstractmethod
async def build(self, *args, **kwargs) -> None:
"""Build/setup the element - must be implemented by subclasses"""
...
@classmethod
async def create(cls, *args, **kwargs) -> Self:
"""Factory method to create and build a card instance"""
instance = cls()
await instance.build(*args, **kwargs)
return instance
class AsyncScrollArea(ui.scroll_area, ABC):
"""Async ScrollArea that inherits from ui.scrol_area for perfect typing"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@abstractmethod
async def build(self, *args, **kwargs) -> None:
"""Build/setup the element - must be implemented by subclasses"""
...
@classmethod
async def create(cls, *args, **kwargs) -> Self:
"""Factory method to create and build a column instance"""
instance = cls()
await instance.build(*args, **kwargs)
return instance

View File

@@ -0,0 +1,4 @@
from .auto_scroll_area import AutoScrollArea
from .chat_input import ChatInput
__all__ = ['AutoScrollArea', 'ChatInput']

View File

@@ -0,0 +1,72 @@
from typing import Optional
from nicegui import ui
class AutoScrollArea(ui.scroll_area):
"""A scroll area that automatically scrolls to bottom when new content is added
Features:
- Auto-scrolls to bottom when at bottom and new content arrives
- Stops auto-scroll when user scrolls up manually
- Resumes auto-scroll when user scrolls back to bottom
"""
_auto_scroll_enabled: bool = True
_auto_scroll_timer: Optional[ui.timer] = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Set up scroll monitoring
# self._handle_scroll
self.on_scroll(self._handle_scroll_event)
self.on('wheel', self._handle_wheel, ['deltaY'])
# Create timer for auto-scrolling
self._auto_scroll_timer = ui.timer(0.1, lambda: self.scroll_to(percent=1))
self._auto_scroll_timer.activate()
def _scroll_event_test(self, e):
print(e.vertical_percentage)
def _handle_scroll_event(self, event_data):
"""Handle scroll events to detect when user is at bottom"""
if not self._auto_scroll_timer:
print('no timer instantiated.')
return
# If scrolled to bottom (100%), enable auto-scroll
if event_data.vertical_percentage > 0.99: # Using 0.99 for some tolerance
if not self._auto_scroll_timer.active:
self._auto_scroll_timer.activate()
def _handle_wheel(self, event_data):
"""Handle mouse wheel events to detect manual scrolling"""
delta_y = event_data.args['deltaY']
if not self._auto_scroll_timer:
print('no timer instantiated.')
return
# If scrolling up (negative delta), disable auto-scroll
if delta_y < 0:
if self._auto_scroll_timer.active:
self._auto_scroll_timer.deactivate()
def enable_auto_scroll(self):
"""Manually enable auto-scrolling"""
if self._auto_scroll_timer:
if not self._auto_scroll_timer.active:
self._auto_scroll_timer.activate()
def disable_auto_scroll(self):
"""Manually disable auto-scrolling"""
if self._auto_scroll_timer:
if self._auto_scroll_timer.active:
self._auto_scroll_timer.deactivate()
def cleanup(self):
"""Clean up timer when component is destroyed"""
if self._auto_scroll_timer:
self._auto_scroll_timer.deactivate()
self._auto_scroll_timer = None

View File

@@ -0,0 +1,27 @@
from typing import Optional, Callable, Awaitable
from nicegui import ui
class ChatInput(ui.textarea):
def __init__(self,
placeholder: str = 'Type your message...',
on_enter: Optional[Callable[[str], Awaitable[None]]] = None,
*args, **kwargs) -> None:
super().__init__(placeholder=placeholder, *args, **kwargs)
self._on_enter_callback = on_enter
self.classes('flex-grow').props('outlined dense autogrow')
self.on('keydown', self._handle_keydown)
async def _handle_keydown(self, event) -> None:
"""Handle keyboard shortcuts for message input"""
if hasattr(event, 'args') and event.args:
key_event = event.args
if key_event.get('key') == 'Enter' and not key_event.get('shiftKey', False):
# Enter without Shift: Send message
event.args['preventDefault'] = True
if self._on_enter_callback and self.value:
await self._on_enter_callback(self.value)
self.value = ''
# Shift+Enter: Allow default behavior (new line)