瀏覽代碼

add toast component (#3186)

Thomas Brandého 1 年之前
父節點
當前提交
1817c30e22

+ 1 - 0
reflex/__init__.py

@@ -111,6 +111,7 @@ _ALL_COMPONENTS = [
     "ordered_list",
     "moment",
     "logo",
+    "toast",
 ]
 
 _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 moment as moment
 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 NoSSRComponent as NoSSRComponent
 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 .radix import *
 from .react_player import *
+from .sonner import *
 from .suneditor import *
 
 icon = lucide.icon

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

@@ -23,9 +23,7 @@ class List(ChakraComponent):
     style_type: Var[str]
 
     @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.
 
         Args:

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

@@ -18,7 +18,7 @@ class List(ChakraComponent):
     def create(  # type: ignore
         cls,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: 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
         cls,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: 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
         cls,
         *children,
-        items: Optional[list | Var[list] | None] = None,
+        items: Optional[Union[Var[list], list]] = None,
         spacing: Optional[Union[Var[str], str]] = None,
         style_position: 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
         cls,
         *children,
-        index: Optional[Var[int]] = None,
+        index: Optional[Union[Var[int], int]] = None,
         name: Optional[Union[Var[str], str]] = None,
         style: Optional[Style] = None,
         key: Optional[Any] = None,

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

@@ -314,10 +314,10 @@ class AccordionRoot(AccordionComponent):
     type: Var[LiteralAccordionType]
 
     # 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.
-    default_value: Var[Optional[Union[str, List[str]]]]
+    default_value: Var[Union[str, List[str]]]
 
     # Whether or not the accordion is collapsible.
     collapsible: Var[bool]

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

@@ -303,8 +303,8 @@ class AccordionItem(AccordionComponent):
     def create(  # type: ignore
         cls,
         *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,
         disabled: 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
         cls,
         *children,
-        color_mode: Optional[LiteralAppearance | None] = None,
+        color_mode: Optional[Literal["inherit", "light", "dark"]] = None,
         theme_panel: Optional[bool] = False,
         has_background: Optional[Union[Var[bool], bool]] = None,
         appearance: Optional[

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

@@ -109,7 +109,9 @@ class ColorModeIconButton(IconButton):
     def create(  # type: ignore
         cls,
         *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,
         size: Optional[
             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(
         cls,
         *children,
-        items: Optional[Union[Var[Iterable], Iterable]] = None,
+        items: Optional[Var[Iterable]] = None,
         **props,
     ):
         """Create a list component.
@@ -68,7 +68,7 @@ class BaseList(Component):
             if isinstance(items, Var):
                 children = [Foreach.create(items, ListItem.create)]
             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["direction"] = "column"
         style = props.setdefault("style", {})

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

@@ -40,7 +40,7 @@ class BaseList(Component):
     def create(  # type: ignore
         cls,
         *children,
-        items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
+        items: Optional[Union[Var[Iterable], Iterable]] = None,
         list_style_type: Optional[
             Union[
                 Var[
@@ -600,7 +600,7 @@ class List(ComponentNamespace):
     @staticmethod
     def __call__(
         *children,
-        items: Optional[Union[Union[Var[Iterable], Iterable], Iterable]] = None,
+        items: Optional[Union[Var[Iterable], Iterable]] = None,
         list_style_type: Optional[
             Union[
                 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 = ""
     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:
         inner_container_type_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}]"
     elif isinstance(value, str):
         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 = (
             _get_type_hint(ev, type_hint_globals, is_optional=False)
             if ev.__name__ == "Var"
@@ -424,7 +461,58 @@ def _generate_component_create_functiondef(
     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(
+    node: ast.ClassDef | None,
     clz_name: str,
     classes: dict[str, type[Component] | type[SimpleNamespace]],
     type_hint_globals: dict[str, Any],
@@ -432,6 +520,7 @@ def _generate_namespace_call_functiondef(
     """Generate the __call__ function definition for a SimpleNamespace.
 
     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.
         classes: Map name to actual class definition.
         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]
 
+    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
     component_clz = clz.__call__.__self__
 
-    # Only generate for create functions
     if clz.__call__.__func__.__name__ != "create":
         return None
 
@@ -603,6 +694,7 @@ class StubGenerator(ast.NodeTransformer):
                 if not child.targets[:]:
                     node.body.remove(child)
                 call_definition = _generate_namespace_call_functiondef(
+                    node,
                     self.current_class,
                     self.classes,
                     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)
 
 
+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:
     """Check if a class is a Union.