Pārlūkot izejas kodu

add toast component (#3186)

Thomas Brandého 1 gadu atpakaļ
vecāks
revīzija
1817c30e22

+ 1 - 0
reflex/__init__.py

@@ -111,6 +111,7 @@ _ALL_COMPONENTS = [
     "ordered_list",
     "ordered_list",
     "moment",
     "moment",
     "logo",
     "logo",
+    "toast",
 ]
 ]
 
 
 _MAPPING = {
 _MAPPING = {

+ 1 - 0
reflex/__init__.pyi

@@ -98,6 +98,7 @@ from reflex.components import unordered_list as unordered_list
 from reflex.components import ordered_list as ordered_list
 from reflex.components import ordered_list as ordered_list
 from reflex.components import moment as moment
 from reflex.components import moment as moment
 from reflex.components import logo as logo
 from reflex.components import logo as logo
+from reflex.components import toast as toast
 from reflex.components.component import Component as Component
 from reflex.components.component import Component as Component
 from reflex.components.component import NoSSRComponent as NoSSRComponent
 from reflex.components.component import NoSSRComponent as NoSSRComponent
 from reflex.components.component import memo as memo
 from reflex.components.component import memo as memo

+ 1 - 0
reflex/components/__init__.py

@@ -15,6 +15,7 @@ from .next import NextLink, next_link
 from .plotly import *
 from .plotly import *
 from .radix import *
 from .radix import *
 from .react_player import *
 from .react_player import *
+from .sonner import *
 from .suneditor import *
 from .suneditor import *
 
 
 icon = lucide.icon
 icon = lucide.icon

+ 1 - 3
reflex/components/chakra/datadisplay/list.py

@@ -23,9 +23,7 @@ class List(ChakraComponent):
     style_type: Var[str]
     style_type: Var[str]
 
 
     @classmethod
     @classmethod
-    def create(
-        cls, *children, items: list | Var[list] | None = None, **props
-    ) -> Component:
+    def create(cls, *children, items: Var[list] | None = None, **props) -> Component:
         """Create a list component.
         """Create a list component.
 
 
         Args:
         Args:

+ 3 - 3
reflex/components/chakra/datadisplay/list.pyi

@@ -18,7 +18,7 @@ class List(ChakraComponent):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,
@@ -178,7 +178,7 @@ class OrderedList(List):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,
@@ -262,7 +262,7 @@ class UnorderedList(List):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_position: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,
         style_type: Optional[Union[Var[str], str]] = None,

+ 1 - 1
reflex/components/chakra/forms/pininput.pyi

@@ -147,7 +147,7 @@ class PinInputField(ChakraComponent):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        index: Optional[Var[int]] = None,
+        index: Optional[Union[Var[int], int]] = None,
         name: Optional[Union[Var[str], str]] = None,
         name: Optional[Union[Var[str], str]] = None,
         style: Optional[Style] = None,
         style: Optional[Style] = None,
         key: Optional[Any] = None,
         key: Optional[Any] = None,

+ 2 - 2
reflex/components/radix/primitives/accordion.py

@@ -314,10 +314,10 @@ class AccordionRoot(AccordionComponent):
     type: Var[LiteralAccordionType]
     type: Var[LiteralAccordionType]
 
 
     # The value of the item to expand.
     # The value of the item to expand.
-    value: Var[Optional[Union[str, List[str]]]]
+    value: Var[Union[str, List[str]]]
 
 
     # The default value of the item to expand.
     # The default value of the item to expand.
-    default_value: Var[Optional[Union[str, List[str]]]]
+    default_value: Var[Union[str, List[str]]]
 
 
     # Whether or not the accordion is collapsible.
     # Whether or not the accordion is collapsible.
     collapsible: Var[bool]
     collapsible: Var[bool]

+ 2 - 2
reflex/components/radix/primitives/accordion.pyi

@@ -303,8 +303,8 @@ class AccordionItem(AccordionComponent):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        header: Optional[Component | Var] = None,
-        content: Optional[Component | Var] = None,
+        header: Optional[Union[Component, Var]] = None,
+        content: Optional[Union[Component, Var]] = None,
         value: Optional[Union[Var[str], str]] = None,
         value: Optional[Union[Var[str], str]] = None,
         disabled: Optional[Union[Var[bool], bool]] = None,
         disabled: Optional[Union[Var[bool], bool]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,

+ 1 - 1
reflex/components/radix/themes/base.pyi

@@ -409,7 +409,7 @@ class Theme(RadixThemesComponent):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        color_mode: Optional[LiteralAppearance | None] = None,
+        color_mode: Optional[Literal["inherit", "light", "dark"]] = None,
         theme_panel: Optional[bool] = False,
         theme_panel: Optional[bool] = False,
         has_background: Optional[Union[Var[bool], bool]] = None,
         has_background: Optional[Union[Var[bool], bool]] = None,
         appearance: Optional[
         appearance: Optional[

+ 3 - 1
reflex/components/radix/themes/color_mode.pyi

@@ -109,7 +109,9 @@ class ColorModeIconButton(IconButton):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        position: Optional[LiteralPosition | None] = None,
+        position: Optional[
+            Literal["top-left", "top-right", "bottom-left", "bottom-right"]
+        ] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
         size: Optional[
         size: Optional[
             Union[Var[Literal["1", "2", "3", "4"]], Literal["1", "2", "3", "4"]]
             Union[Var[Literal["1", "2", "3", "4"]], Literal["1", "2", "3", "4"]]

+ 2 - 2
reflex/components/radix/themes/layout/list.py

@@ -49,7 +49,7 @@ class BaseList(Component):
     def create(
     def create(
         cls,
         cls,
         *children,
         *children,
-        items: Optional[Union[Var[Iterable], Iterable]] = None,
+        items: Optional[Var[Iterable]] = None,
         **props,
         **props,
     ):
     ):
         """Create a list component.
         """Create a list component.
@@ -68,7 +68,7 @@ class BaseList(Component):
             if isinstance(items, Var):
             if isinstance(items, Var):
                 children = [Foreach.create(items, ListItem.create)]
                 children = [Foreach.create(items, ListItem.create)]
             else:
             else:
-                children = [ListItem.create(item) for item in items]
+                children = [ListItem.create(item) for item in items]  # type: ignore
         props["list_style_position"] = "outside"
         props["list_style_position"] = "outside"
         props["direction"] = "column"
         props["direction"] = "column"
         style = props.setdefault("style", {})
         style = props.setdefault("style", {})

+ 2 - 2
reflex/components/radix/themes/layout/list.pyi

@@ -40,7 +40,7 @@ class BaseList(Component):
     def create(  # type: ignore
     def create(  # type: ignore
         cls,
         cls,
         *children,
         *children,
-        items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
+        items: Optional[Union[Var[Iterable], Iterable]] = None,
         list_style_type: Optional[
         list_style_type: Optional[
             Union[
             Union[
                 Var[
                 Var[
@@ -600,7 +600,7 @@ class List(ComponentNamespace):
     @staticmethod
     @staticmethod
     def __call__(
     def __call__(
         *children,
         *children,
-        items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
+        items: Optional[Union[Var[Iterable], Iterable]] = None,
         list_style_type: Optional[
         list_style_type: Optional[
             Union[
             Union[
                 Var[
                 Var[

+ 3 - 0
reflex/components/sonner/__init__.py

@@ -0,0 +1,3 @@
+"""Init file for the sonner component."""
+
+from .toast import toast

+ 267 - 0
reflex/components/sonner/toast.py

@@ -0,0 +1,267 @@
+"""Sonner toast component."""
+
+from __future__ import annotations
+
+from typing import Literal
+
+from reflex.base import Base
+from reflex.components.component import Component, ComponentNamespace
+from reflex.components.lucide.icon import Icon
+from reflex.event import EventSpec, call_script
+from reflex.style import Style, color_mode
+from reflex.utils import format
+from reflex.utils.imports import ImportVar
+from reflex.utils.serializers import serialize
+from reflex.vars import Var, VarData
+
+LiteralPosition = Literal[
+    "top-left",
+    "top-center",
+    "top-right",
+    "bottom-left",
+    "bottom-center",
+    "bottom-right",
+]
+
+
+toast_ref = Var.create_safe("refs['__toast']")
+
+
+class PropsBase(Base):
+    """Base class for all props classes."""
+
+    def json(self) -> str:
+        """Convert the object to a json string.
+
+        Returns:
+            The object as a json string.
+        """
+        from reflex.utils.serializers import serialize
+
+        return self.__config__.json_dumps(
+            {format.to_camel_case(key): value for key, value in self.dict().items()},
+            default=serialize,
+        )
+
+
+class ToastProps(PropsBase):
+    """Props for the toast component."""
+
+    # Toast's description, renders underneath the title.
+    description: str = ""
+
+    # Whether to show the close button.
+    close_button: bool = False
+
+    # Dark toast in light mode and vice versa.
+    invert: bool = False
+
+    # Control the sensitivity of the toast for screen readers
+    important: bool = False
+
+    # Time in milliseconds that should elapse before automatically closing the toast.
+    duration: int = 4000
+
+    # Position of the toast.
+    position: LiteralPosition = "bottom-right"
+
+    # If false, it'll prevent the user from dismissing the toast.
+    dismissible: bool = True
+
+    # TODO: fix serialization of icons for toast? (might not be possible yet)
+    # Icon displayed in front of toast's text, aligned vertically.
+    # icon: Optional[Icon] = None
+
+    # TODO: fix implementation for action / cancel buttons
+    # Renders a primary button, clicking it will close the toast.
+    # action: str = ""
+
+    # Renders a secondary button, clicking it will close the toast.
+    # cancel: str = ""
+
+    # Custom id for the toast.
+    id: str = ""
+
+    # Removes the default styling, which allows for easier customization.
+    unstyled: bool = False
+
+    # Custom style for the toast.
+    style: Style = Style()
+
+    # Custom style for the toast primary button.
+    # action_button_styles: Style = Style()
+
+    # Custom style for the toast secondary button.
+    # cancel_button_styles: Style = Style()
+
+
+class Toaster(Component):
+    """A Toaster Component for displaying toast notifications."""
+
+    library = "sonner@1.4.41"
+
+    tag = "Toaster"
+
+    # the theme of the toast
+    theme: Var[str] = color_mode
+
+    # whether to show rich colors
+    rich_colors: Var[bool] = Var.create_safe(True)
+
+    # whether to expand the toast
+    expand: Var[bool] = Var.create_safe(True)
+
+    # the number of toasts that are currently visible
+    visible_toasts: Var[int]
+
+    # the position of the toast
+    position: Var[LiteralPosition] = Var.create_safe("bottom-right")
+
+    # whether to show the close button
+    close_button: Var[bool] = Var.create_safe(False)
+
+    # offset of the toast
+    offset: Var[str]
+
+    # directionality of the toast (default: ltr)
+    dir: Var[str]
+
+    # Keyboard shortcut that will move focus to the toaster area.
+    hotkey: Var[str]
+
+    # Dark toasts in light mode and vice versa.
+    invert: Var[bool]
+
+    # These will act as default options for all toasts. See toast() for all available options.
+    toast_options: Var[ToastProps]
+
+    # Gap between toasts when expanded
+    gap: Var[int]
+
+    # Changes the default loading icon
+    loading_icon: Var[Icon]
+
+    # Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
+    pause_when_page_is_hidden: Var[bool]
+
+    def _get_hooks(self) -> Var[str]:
+        hook = Var.create_safe(f"{toast_ref} = toast", _var_is_local=True)
+        hook._var_data = VarData(  # type: ignore
+            imports={
+                "/utils/state": [ImportVar(tag="refs")],
+                self.library: [ImportVar(tag="toast", install=False)],
+            }
+        )
+        return hook
+
+    @staticmethod
+    def send_toast(message: str, level: str | None = None, **props) -> EventSpec:
+        """Send a toast message.
+
+        Args:
+            message: The message to display.
+            level: The level of the toast.
+            **props: The options for the toast.
+
+        Returns:
+            The toast event.
+        """
+        toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref
+        if props:
+            args = serialize(ToastProps(**props))
+            toast = f"{toast_command}(`{message}`, {args})"
+        else:
+            toast = f"{toast_command}(`{message}`)"
+
+        toast_action = Var.create(toast, _var_is_string=False, _var_is_local=True)
+        return call_script(toast_action)  # type: ignore
+
+    @staticmethod
+    def toast_info(message: str, **kwargs):
+        """Display an info toast message.
+
+        Args:
+            message: The message to display.
+            kwargs: Additional toast props.
+
+        Returns:
+            The toast event.
+        """
+        return Toaster.send_toast(message, level="info", **kwargs)
+
+    @staticmethod
+    def toast_warning(message: str, **kwargs):
+        """Display a warning toast message.
+
+        Args:
+            message: The message to display.
+            kwargs: Additional toast props.
+
+        Returns:
+            The toast event.
+        """
+        return Toaster.send_toast(message, level="warning", **kwargs)
+
+    @staticmethod
+    def toast_error(message: str, **kwargs):
+        """Display an error toast message.
+
+        Args:
+            message: The message to display.
+            kwargs: Additional toast props.
+
+        Returns:
+            The toast event.
+        """
+        return Toaster.send_toast(message, level="error", **kwargs)
+
+    @staticmethod
+    def toast_success(message: str, **kwargs):
+        """Display a success toast message.
+
+        Args:
+            message: The message to display.
+            kwargs: Additional toast props.
+
+        Returns:
+            The toast event.
+        """
+        return Toaster.send_toast(message, level="success", **kwargs)
+
+    def toast_dismiss(self, id: str | None):
+        """Dismiss a toast.
+
+        Args:
+            id: The id of the toast to dismiss.
+
+        Returns:
+            The toast dismiss event.
+        """
+        if id is None:
+            dismiss = f"{toast_ref}.dismiss()"
+        else:
+            dismiss = f"{toast_ref}.dismiss({id})"
+        dismiss_action = Var.create(dismiss, _var_is_string=False, _var_is_local=True)
+        return call_script(dismiss_action)  # type: ignore
+
+
+# TODO: figure out why loading toast stay open forever
+# def toast_loading(message: str, **kwargs):
+#     return _toast(message, level="loading", **kwargs)
+
+
+class ToastNamespace(ComponentNamespace):
+    """Namespace for toast components."""
+
+    provider = staticmethod(Toaster.create)
+    options = staticmethod(ToastProps)
+    info = staticmethod(Toaster.toast_info)
+    warning = staticmethod(Toaster.toast_warning)
+    error = staticmethod(Toaster.toast_error)
+    success = staticmethod(Toaster.toast_success)
+    dismiss = staticmethod(Toaster.toast_dismiss)
+    # loading = staticmethod(toast_loading)
+    __call__ = staticmethod(Toaster.send_toast)
+
+
+toast = ToastNamespace()

+ 205 - 0
reflex/components/sonner/toast.pyi

@@ -0,0 +1,205 @@
+"""Stub file for reflex/components/sonner/toast.py"""
+# ------------------- DO NOT EDIT ----------------------
+# This file was generated by `reflex/utils/pyi_generator.py`!
+# ------------------------------------------------------
+
+from typing import Any, Dict, Literal, Optional, Union, overload
+from reflex.vars import Var, BaseVar, ComputedVar
+from reflex.event import EventChain, EventHandler, EventSpec
+from reflex.style import Style
+from typing import Literal
+from reflex.base import Base
+from reflex.components.component import Component, ComponentNamespace
+from reflex.components.lucide.icon import Icon
+from reflex.event import EventSpec, call_script
+from reflex.style import Style, color_mode
+from reflex.utils import format
+from reflex.utils.imports import ImportVar
+from reflex.utils.serializers import serialize
+from reflex.vars import Var, VarData
+
+LiteralPosition = Literal[
+    "top-left",
+    "top-center",
+    "top-right",
+    "bottom-left",
+    "bottom-center",
+    "bottom-right",
+]
+toast_ref = Var.create_safe("refs['__toast']")
+
+class PropsBase(Base):
+    def json(self) -> str: ...
+
+class ToastProps(PropsBase):
+    description: str
+    close_button: bool
+    invert: bool
+    important: bool
+    duration: int
+    position: LiteralPosition
+    dismissible: bool
+    id: str
+    unstyled: bool
+    style: Style
+
+class Toaster(Component):
+    @staticmethod
+    def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ...
+    @staticmethod
+    def toast_info(message: str, **kwargs): ...
+    @staticmethod
+    def toast_warning(message: str, **kwargs): ...
+    @staticmethod
+    def toast_error(message: str, **kwargs): ...
+    @staticmethod
+    def toast_success(message: str, **kwargs): ...
+    def toast_dismiss(self, id: str | None): ...
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        theme: Optional[Union[Var[str], str]] = None,
+        rich_colors: Optional[Union[Var[bool], bool]] = None,
+        expand: Optional[Union[Var[bool], bool]] = None,
+        visible_toasts: Optional[Union[Var[int], int]] = None,
+        position: Optional[
+            Union[
+                Var[
+                    Literal[
+                        "top-left",
+                        "top-center",
+                        "top-right",
+                        "bottom-left",
+                        "bottom-center",
+                        "bottom-right",
+                    ]
+                ],
+                Literal[
+                    "top-left",
+                    "top-center",
+                    "top-right",
+                    "bottom-left",
+                    "bottom-center",
+                    "bottom-right",
+                ],
+            ]
+        ] = None,
+        close_button: Optional[Union[Var[bool], bool]] = None,
+        offset: Optional[Union[Var[str], str]] = None,
+        dir: Optional[Union[Var[str], str]] = None,
+        hotkey: Optional[Union[Var[str], str]] = None,
+        invert: Optional[Union[Var[bool], bool]] = None,
+        toast_options: Optional[Union[Var[ToastProps], ToastProps]] = None,
+        gap: Optional[Union[Var[int], int]] = None,
+        loading_icon: Optional[Union[Var[Icon], Icon]] = None,
+        pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
+        style: Optional[Style] = None,
+        key: Optional[Any] = None,
+        id: Optional[Any] = None,
+        class_name: Optional[Any] = None,
+        autofocus: Optional[bool] = None,
+        custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
+        on_blur: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_click: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_context_menu: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_double_click: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_focus: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mount: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_down: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_enter: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_leave: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_move: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_out: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_over: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_mouse_up: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_scroll: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        on_unmount: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
+        **props
+    ) -> "Toaster":
+        """Create the component.
+
+        Args:
+            *children: The children of the component.
+            theme: the theme of the toast
+            rich_colors: whether to show rich colors
+            expand: whether to expand the toast
+            visible_toasts: the number of toasts that are currently visible
+            position: the position of the toast
+            close_button: whether to show the close button
+            offset: offset of the toast
+            dir: directionality of the toast (default: ltr)
+            hotkey: Keyboard shortcut that will move focus to the toaster area.
+            invert: Dark toasts in light mode and vice versa.
+            toast_options: These will act as default options for all toasts. See toast() for all available options.
+            gap: Gap between toasts when expanded
+            loading_icon: Changes the default loading icon
+            pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
+            style: The style of the component.
+            key: A unique key for the component.
+            id: The id for the component.
+            class_name: The class name for the component.
+            autofocus: Whether the component should take the focus once the page is loaded
+            custom_attrs: custom attribute
+            **props: The props of the component.
+
+        Returns:
+            The component.
+        """
+        ...
+
+class ToastNamespace(ComponentNamespace):
+    provider = staticmethod(Toaster.create)
+    options = staticmethod(ToastProps)
+    info = staticmethod(Toaster.toast_info)
+    warning = staticmethod(Toaster.toast_warning)
+    error = staticmethod(Toaster.toast_error)
+    success = staticmethod(Toaster.toast_success)
+    dismiss = staticmethod(Toaster.toast_dismiss)
+
+    @staticmethod
+    def __call__(message: str, level: Optional[str], **props) -> "Optional[EventSpec]":
+        """Send a toast message.
+
+        Args:
+            message: The message to display.
+            level: The level of the toast.
+            **props: The options for the toast.
+
+        Returns:
+            The toast event.
+        """
+        ...
+
+toast = ToastNamespace()

+ 93 - 1
reflex/utils/pyi_generator.py

@@ -117,6 +117,29 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
     """
     """
     res = ""
     res = ""
     args = get_args(value)
     args = get_args(value)
+
+    if value is type(None):
+        return "None"
+
+    if rx_types.is_union(value):
+        if type(None) in value.__args__:
+            res_args = [
+                _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
+                for arg in value.__args__
+                if arg is not type(None)
+            ]
+            if len(res_args) == 1:
+                return f"Optional[{res_args[0]}]"
+            else:
+                res = f"Union[{', '.join(res_args)}]"
+                return f"Optional[{res}]"
+
+        res_args = [
+            _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
+            for arg in value.__args__
+        ]
+        return f"Union[{', '.join(res_args)}]"
+
     if args:
     if args:
         inner_container_type_args = (
         inner_container_type_args = (
             [repr(arg) for arg in args]
             [repr(arg) for arg in args]
@@ -141,6 +164,20 @@ def _get_type_hint(value, type_hint_globals, is_optional=True) -> str:
                 res = f"Union[{res}]"
                 res = f"Union[{res}]"
     elif isinstance(value, str):
     elif isinstance(value, str):
         ev = eval(value, type_hint_globals)
         ev = eval(value, type_hint_globals)
+        if rx_types.is_optional(ev):
+            # hints = {
+            #     _get_type_hint(arg, type_hint_globals, is_optional=False)
+            #     for arg in ev.__args__
+            # }
+            return _get_type_hint(ev, type_hint_globals, is_optional=False)
+            # return f"Optional[{', '.join(hints)}]"
+
+        if rx_types.is_union(ev):
+            res = [
+                _get_type_hint(arg, type_hint_globals, rx_types.is_optional(arg))
+                for arg in ev.__args__
+            ]
+            return f"Union[{', '.join(res)}]"
         res = (
         res = (
             _get_type_hint(ev, type_hint_globals, is_optional=False)
             _get_type_hint(ev, type_hint_globals, is_optional=False)
             if ev.__name__ == "Var"
             if ev.__name__ == "Var"
@@ -424,7 +461,58 @@ def _generate_component_create_functiondef(
     return definition
     return definition
 
 
 
 
+def _generate_staticmethod_call_functiondef(
+    node: ast.FunctionDef | None,
+    clz: type[Component] | type[SimpleNamespace],
+    type_hint_globals: dict[str, Any],
+) -> ast.FunctionDef | None:
+    ...
+
+    fullspec = getfullargspec(clz.__call__)
+
+    call_args = ast.arguments(
+        args=[
+            ast.arg(
+                name,
+                annotation=ast.Name(
+                    id=_get_type_hint(
+                        anno := fullspec.annotations[name],
+                        type_hint_globals,
+                        is_optional=rx_types.is_optional(anno),
+                    )
+                ),
+            )
+            for name in fullspec.args
+        ],
+        posonlyargs=[],
+        kwonlyargs=[],
+        kw_defaults=[],
+        kwarg=ast.arg(arg="props"),
+        defaults=[],
+    )
+    definition = ast.FunctionDef(
+        name="__call__",
+        args=call_args,
+        body=[
+            ast.Expr(value=ast.Constant(value=clz.__call__.__doc__)),
+            ast.Expr(
+                value=ast.Constant(...),
+            ),
+        ],
+        decorator_list=[ast.Name(id="staticmethod")],
+        lineno=node.lineno if node is not None else None,
+        returns=ast.Constant(
+            value=_get_type_hint(
+                typing.get_type_hints(clz.__call__).get("return", None),
+                type_hint_globals,
+            )
+        ),
+    )
+    return definition
+
+
 def _generate_namespace_call_functiondef(
 def _generate_namespace_call_functiondef(
+    node: ast.ClassDef | None,
     clz_name: str,
     clz_name: str,
     classes: dict[str, type[Component] | type[SimpleNamespace]],
     classes: dict[str, type[Component] | type[SimpleNamespace]],
     type_hint_globals: dict[str, Any],
     type_hint_globals: dict[str, Any],
@@ -432,6 +520,7 @@ def _generate_namespace_call_functiondef(
     """Generate the __call__ function definition for a SimpleNamespace.
     """Generate the __call__ function definition for a SimpleNamespace.
 
 
     Args:
     Args:
+        node: The existing __call__ classdef parent node from the ast
         clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for.
         clz_name: The name of the SimpleNamespace class to generate the __call__ functiondef for.
         classes: Map name to actual class definition.
         classes: Map name to actual class definition.
         type_hint_globals: The globals to use to resolving a type hint str.
         type_hint_globals: The globals to use to resolving a type hint str.
@@ -446,10 +535,12 @@ def _generate_namespace_call_functiondef(
 
 
     clz = classes[clz_name]
     clz = classes[clz_name]
 
 
+    if not hasattr(clz.__call__, "__self__"):
+        return _generate_staticmethod_call_functiondef(node, clz, type_hint_globals)  # type: ignore
+
     # Determine which class is wrapped by the namespace __call__ method
     # Determine which class is wrapped by the namespace __call__ method
     component_clz = clz.__call__.__self__
     component_clz = clz.__call__.__self__
 
 
-    # Only generate for create functions
     if clz.__call__.__func__.__name__ != "create":
     if clz.__call__.__func__.__name__ != "create":
         return None
         return None
 
 
@@ -603,6 +694,7 @@ class StubGenerator(ast.NodeTransformer):
                 if not child.targets[:]:
                 if not child.targets[:]:
                     node.body.remove(child)
                     node.body.remove(child)
                 call_definition = _generate_namespace_call_functiondef(
                 call_definition = _generate_namespace_call_functiondef(
+                    node,
                     self.current_class,
                     self.current_class,
                     self.classes,
                     self.classes,
                     type_hint_globals=self.type_hint_globals,
                     type_hint_globals=self.type_hint_globals,

+ 12 - 0
reflex/utils/types.py

@@ -126,6 +126,18 @@ def is_generic_alias(cls: GenericType) -> bool:
     return isinstance(cls, GenericAliasTypes)
     return isinstance(cls, GenericAliasTypes)
 
 
 
 
+def is_none(cls: GenericType) -> bool:
+    """Check if a class is None.
+
+    Args:
+        cls: The class to check.
+
+    Returns:
+        Whether the class is None.
+    """
+    return cls is type(None) or cls is None
+
+
 def is_union(cls: GenericType) -> bool:
 def is_union(cls: GenericType) -> bool:
     """Check if a class is a Union.
     """Check if a class is a Union.