Răsfoiți Sursa

Radix Accordion (#2310)

Alek Petuskey 1 an în urmă
părinte
comite
d466c2aaa2

+ 1 - 1
reflex/.templates/jinja/web/pages/utils.js.jinja2

@@ -85,7 +85,7 @@
 {% macro render_match_tag(component) %}
 {
     (() => {
-        switch (JSON.stringify({{ component.cond._var_full_name }})) {
+        switch (JSON.stringify({{ component.cond._var_name_unwrapped }})) {
         {% for case in component.match_cases %}
             {% for condition in case[:-1] %}
                 case JSON.stringify({{ condition._var_name_unwrapped }}):

+ 3 - 1
reflex/components/component.py

@@ -623,6 +623,8 @@ class Component(BaseComponent, ABC):
         Returns:
             The dictionary of the component style as value and the style notation as key.
         """
+        if isinstance(self.style, Var):
+            return {"css": self.style}
         return {"css": Var.create(format_as_emotion(self.style))}
 
     def render(self) -> Dict:
@@ -721,7 +723,7 @@ class Component(BaseComponent, ABC):
                 vars.append(prop_var)
 
         # Style keeps track of its own VarData instance, so embed in a temp Var that is yielded.
-        if self.style:
+        if isinstance(self.style, dict) and self.style or isinstance(self.style, Var):
             vars.append(
                 BaseVar(
                     _var_name="style",

+ 8 - 4
reflex/components/core/match.py

@@ -64,7 +64,8 @@ class Match(MemoizationLeaf):
         Raises:
             ValueError: If the condition is not provided.
         """
-        match_cond_var = Var.create(cond)
+        match_cond_var = Var.create(cond, _var_is_string=type(cond) is str)
+
         if match_cond_var is None:
             raise ValueError("The condition must be set")
         return match_cond_var  # type: ignore
@@ -216,13 +217,14 @@ class Match(MemoizationLeaf):
 
         return match_cond_var._replace(
             _var_name=format.format_match(
-                cond=match_cond_var._var_full_name,
+                cond=match_cond_var._var_name_unwrapped,
                 match_cases=match_cases,  # type: ignore
                 default=default,  # type: ignore
             ),
             _var_type=default._var_type,  # type: ignore
             _var_is_local=False,
             _var_full_name_needs_state_prefix=False,
+            _var_is_string=False,
             merge_var_data=VarData.merge(*var_data),
         )
 
@@ -247,11 +249,13 @@ class Match(MemoizationLeaf):
         for case in self.match_cases:
             if isinstance(case[-1], BaseComponent):
                 merged_imports = imports.merge_imports(
-                    merged_imports, case[-1].get_imports()
+                    merged_imports,
+                    case[-1].get_imports(),
                 )
         # Get the import of the default case component.
         if isinstance(self.default, BaseComponent):
             merged_imports = imports.merge_imports(
-                merged_imports, self.default.get_imports()
+                merged_imports,
+                self.default.get_imports(),
             )
         return merged_imports

+ 14 - 1
reflex/components/radix/primitives/__init__.py

@@ -1,6 +1,12 @@
 """Radix primitive components (https://www.radix-ui.com/primitives)."""
 
-from .accordion import accordion, accordion_item
+from .accordion import (
+    AccordionContent,
+    AccordionHeader,
+    AccordionRoot,
+    AccordionTrigger,
+    accordion_item,
+)
 from .form import (
     form_control,
     form_field,
@@ -12,3 +18,10 @@ from .form import (
 )
 from .progress import progress
 from .slider import slider
+
+# accordion
+accordion = AccordionRoot.create
+accordion_root = AccordionRoot.create
+accordion_header = AccordionHeader.create
+accordion_trigger = AccordionTrigger.create
+accordion_content = AccordionContent.create

+ 425 - 69
reflex/components/radix/primitives/accordion.py

@@ -1,22 +1,370 @@
 """Radix accordion components."""
 
-from typing import Literal
+from typing import Any, Dict, Literal
 
+from reflex.components.base.fragment import Fragment
 from reflex.components.component import Component
+from reflex.components.core import cond, match
 from reflex.components.radix.primitives.base import RadixPrimitiveComponent
 from reflex.components.radix.themes.components.icons import Icon
-from reflex.style import Style
+from reflex.style import (
+    Style,
+    convert_dict_to_style_and_format_emotion,
+    format_as_emotion,
+)
 from reflex.utils import imports
-from reflex.vars import Var
+from reflex.vars import BaseVar, Var
 
 LiteralAccordionType = Literal["single", "multiple"]
 LiteralAccordionDir = Literal["ltr", "rtl"]
 LiteralAccordionOrientation = Literal["vertical", "horizontal"]
-
+LiteralAccordionRootVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
+LiteralAccordionRootColorScheme = Literal["primary", "accent"]
 
 DEFAULT_ANIMATION_DURATION = 250
 
 
+def get_theme_accordion_root(variant: Var[str], color_scheme: Var[str]) -> BaseVar:
+    """Get the theme for the accordion root component.
+
+    Args:
+        variant: The variant of the accordion.
+        color_scheme: The color of the accordion.
+
+    Returns:
+        The theme for the accordion root component.
+    """
+    return match(  # type: ignore
+        variant,
+        (
+            "soft",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "border_radius": "6px",
+                    "background_color": cond(
+                        color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
+                    ),
+                    "box_shadow": "0 2px 10px var(--black-a1)",
+                }
+            ),
+        ),
+        (
+            "outline",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "border_radius": "6px",
+                    "border": cond(
+                        color_scheme == "primary",
+                        "1px solid var(--accent-6)",
+                        "1px solid var(--slate-6)",
+                    ),
+                    "box_shadow": "0 2px 10px var(--black-a1)",
+                }
+            ),
+        ),
+        (
+            "surface",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "border_radius": "6px",
+                    "border": cond(
+                        color_scheme == "primary",
+                        "1px solid var(--accent-6)",
+                        "1px solid var(--slate-6)",
+                    ),
+                    "background_color": cond(
+                        color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
+                    ),
+                    "box_shadow": "0 2px 10px var(--black-a1)",
+                }
+            ),
+        ),
+        (
+            "ghost",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "border_radius": "6px",
+                    "background_color": "none",
+                    "box_shadow": "None",
+                }
+            ),
+        ),
+        convert_dict_to_style_and_format_emotion(
+            {
+                "border_radius": "6px",
+                "background_color": cond(
+                    color_scheme == "primary", "var(--accent-9)", "var(--slate-9)"
+                ),
+                "box_shadow": "0 2px 10px var(--black-a4)",
+            }
+        )
+        # defaults to classic
+    )
+
+
+def get_theme_accordion_item():
+    """Get the theme for the accordion item component.
+
+    Returns:
+        The theme for the accordion item component.
+    """
+    return convert_dict_to_style_and_format_emotion(
+        {
+            "overflow": "hidden",
+            "width": "100%",
+            "margin_top": "1px",
+            # "background_color": "var(--accent-3)",
+            # "background_color": cond(
+            #     color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
+            # ),
+            "&:first-child": {
+                "margin_top": 0,
+                "border_top_left_radius": "4px",
+                "border_top_right_radius": "4px",
+            },
+            "&:last-child": {
+                "border_bottom_left_radius": "4px",
+                "border_bottom_right_radius": "4px",
+            },
+            "&:focus-within": {
+                "position": "relative",
+                "z_index": 1,
+            },
+        }
+    )
+
+
+def get_theme_accordion_header() -> dict[str, str]:
+    """Get the theme for the accordion header component.
+
+    Returns:
+        The theme for the accordion header component.
+    """
+    return {
+        "display": "flex",
+    }
+
+
+def get_theme_accordion_trigger(variant: str | Var, color_scheme: str | Var) -> BaseVar:
+    """Get the theme for the accordion trigger component.
+
+    Args:
+        variant: The variant of the accordion.
+        color_scheme: The color of the accordion.
+
+    Returns:
+        The theme for the accordion trigger component.
+    """
+    return match(  # type: ignore
+        variant,
+        (
+            "soft",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "color": cond(
+                        color_scheme == "primary",
+                        "var(--accent-9-contrast)",
+                        "var(--slate-9-contrast)",
+                    ),
+                    "&:hover": {
+                        "background_color": cond(
+                            color_scheme == "primary",
+                            "var(--accent-4)",
+                            "var(--slate-4)",
+                        ),
+                    },
+                    "& > .AccordionChevron": {
+                        "color": cond(
+                            color_scheme == "primary",
+                            "var(--accent-11)",
+                            "var(--slate-11)",
+                        ),
+                        "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                    },
+                    "&[data-state='open'] > .AccordionChevron": {
+                        "transform": "rotate(180deg)",
+                    },
+                    "font_family": "inherit",
+                    "width": "100%",
+                    "padding": "0 20px",
+                    "height": "45px",
+                    "flex": 1,
+                    "display": "flex",
+                    "align_items": "center",
+                    "justify_content": "space-between",
+                    "font_size": "15px",
+                    "box_shadow": "0 1px 0 var(--accent-6)",
+                    "line_height": 1,
+                }
+            ),
+        ),
+        (
+            "outline",
+            "surface",
+            "ghost",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "color": cond(
+                        color_scheme == "primary",
+                        "var(--accent-11)",
+                        "var(--slate-11)",
+                    ),
+                    "&:hover": {
+                        "background_color": cond(
+                            color_scheme == "primary",
+                            "var(--accent-4)",
+                            "var(--slate-4)",
+                        ),
+                    },
+                    "& > .AccordionChevron": {
+                        "color": cond(
+                            color_scheme == "primary",
+                            "var(--accent-11)",
+                            "var(--slate-11)",
+                        ),
+                        "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                    },
+                    "&[data-state='open'] > .AccordionChevron": {
+                        "transform": "rotate(180deg)",
+                    },
+                    "font_family": "inherit",
+                    "width": "100%",
+                    "padding": "0 20px",
+                    "height": "45px",
+                    "flex": 1,
+                    "display": "flex",
+                    "align_items": "center",
+                    "justify_content": "space-between",
+                    "font_size": "15px",
+                    "box_shadow": "0 1px 0 var(--accent-6)",
+                    "line_height": 1,
+                }
+            ),
+        ),
+        # defaults to classic
+        convert_dict_to_style_and_format_emotion(
+            {
+                "color": cond(
+                    color_scheme == "primary",
+                    "var(--accent-9-contrast)",
+                    "var(--slate-9-contrast)",
+                ),
+                "box_shadow": "0 1px 0 var(--accent-6)",
+                "&:hover": {
+                    "background_color": cond(
+                        color_scheme == "primary", "var(--accent-10)", "var(--slate-10)"
+                    ),
+                },
+                "& > .AccordionChevron": {
+                    "color": cond(
+                        color_scheme == "primary",
+                        "var(--accent-9-contrast)",
+                        "var(--slate-9-contrast)",
+                    ),
+                    "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                },
+                "&[data-state='open'] > .AccordionChevron": {
+                    "transform": "rotate(180deg)",
+                },
+                "font_family": "inherit",
+                "width": "100%",
+                "padding": "0 20px",
+                "height": "45px",
+                "flex": 1,
+                "display": "flex",
+                "align_items": "center",
+                "justify_content": "space-between",
+                "font_size": "15px",
+                "line_height": 1,
+            }
+        ),
+    )
+
+
+def get_theme_accordion_content(variant: str | Var, color_scheme: str | Var) -> BaseVar:
+    """Get the theme for the accordion content component.
+
+    Args:
+        variant: The variant of the accordion.
+        color_scheme: The color of the accordion.
+
+    Returns:
+        The theme for the accordion content component.
+    """
+    return match(  # type: ignore
+        variant,
+        (
+            "outline",
+            "ghost",
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "overflow": "hidden",
+                    "font_size": "10px",
+                    "color": cond(
+                        color_scheme == "primary",
+                        "var(--accent-9-contrast)",
+                        "var(--slate-9-contrast)",
+                    ),
+                    "background_color": cond(
+                        color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
+                    ),
+                    "padding": "15px, 20px",
+                    "&[data-state='open']": {
+                        "animation": Var.create(
+                            f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                            _var_is_string=True,
+                        ),
+                    },
+                    "&[data-state='closed']": {
+                        "animation": Var.create(
+                            f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                            _var_is_string=True,
+                        ),
+                    },
+                }
+            ),
+        ),
+        convert_dict_to_style_and_format_emotion(
+            {
+                "overflow": "hidden",
+                "font_size": "10px",
+                "color": cond(
+                    color_scheme == "primary",
+                    "var(--accent-9-contrast)",
+                    "var(--slate-9-contrast)",
+                ),
+                "background_color": match(
+                    variant,
+                    (
+                        "classic",
+                        cond(
+                            color_scheme == "primary",
+                            "var(--accent-9)",
+                            "var(--slate-9)",
+                        ),
+                    ),
+                    cond(
+                        color_scheme == "primary", "var(--accent-3)", "var(--slate-3)"
+                    ),
+                ),
+                "padding": "15px, 20px",
+                "&[data-state='open']": {
+                    "animation": Var.create(
+                        f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                        _var_is_string=True,
+                    ),
+                },
+                "&[data-state='closed']": {
+                    "animation": Var.create(
+                        f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
+                        _var_is_string=True,
+                    ),
+                },
+            }
+        ),
+    )
+
+
 class AccordionComponent(RadixPrimitiveComponent):
     """Base class for all @radix-ui/accordion components."""
 
@@ -51,16 +399,79 @@ class AccordionRoot(AccordionComponent):
     # The orientation of the accordion.
     orientation: Var[LiteralAccordionOrientation]
 
+    # The variant of the accordion.
+    variant: Var[LiteralAccordionRootVariant] = "classic"  # type: ignore
+
+    # The color scheme of the accordion.
+    color_scheme: Var[LiteralAccordionRootColorScheme] = "primary"  # type: ignore
+
+    # dynamic themes of the accordion generated at compile time.
+    _dynamic_themes: Var[dict]
+
+    @classmethod
+    def create(cls, *children, **props) -> Component:
+        """Create the Accordion root component.
+
+        Args:
+            *children: The children of the component.
+            **props: The properties of the component.
+
+        Returns:
+            The Accordion root Component.
+        """
+        comp = super().create(*children, **props)
+
+        if not comp.color_scheme._var_state:  # type: ignore
+            # mark the vars of color string literals as strings so they can be formatted properly when performing a var operation.
+            comp.color_scheme._var_is_string = True  # type: ignore
+
+        if not comp.variant._var_state:  # type: ignore
+            # mark the vars of variant string literals as strings so they are formatted properly in the match condition.
+            comp.variant._var_is_string = True  # type: ignore
+
+        # remove Fragment and cond wrap workaround when https://github.com/reflex-dev/reflex/issues/2393 is resolved.
+        return Fragment.create(comp, cond(True, Fragment.create()))
+
+    def _get_style(self) -> dict:
+        """Get the style for the component.
+
+        Returns:
+            The dictionary of the component style as value and the style notation as key.
+        """
+        return {"css": self._dynamic_themes._merge(format_as_emotion(self.style))}  # type: ignore
+
     def _apply_theme(self, theme: Component):
-        self.style = Style(
-            {
-                "border_radius": "6px",
-                "background_color": "var(--accent-6)",
-                "box_shadow": "0 2px 10px var(--black-a4)",
-                **self.style,
-            }
+
+        self._dynamic_themes = Var.create(  # type: ignore
+            convert_dict_to_style_and_format_emotion(
+                {
+                    "& .AccordionItem": get_theme_accordion_item(),
+                    "& .AccordionHeader": get_theme_accordion_header(),
+                    "& .AccordionTrigger": get_theme_accordion_trigger(
+                        variant=self.variant, color_scheme=self.color_scheme
+                    ),
+                    "& .AccordionContent": get_theme_accordion_content(
+                        variant=self.variant, color_scheme=self.color_scheme
+                    ),
+                }
+            )
+        )._merge(  # type: ignore
+            get_theme_accordion_root(
+                variant=self.variant, color_scheme=self.color_scheme
+            )
         )
 
+    def get_event_triggers(self) -> Dict[str, Any]:
+        """Get the events triggers signatures for the component.
+
+        Returns:
+            The signatures of the event triggers.
+        """
+        return {
+            **super().get_event_triggers(),
+            "on_value_change": lambda e0: [e0],
+        }
+
 
 class AccordionItem(AccordionComponent):
     """An accordion component."""
@@ -78,22 +489,6 @@ class AccordionItem(AccordionComponent):
     def _apply_theme(self, theme: Component):
         self.style = Style(
             {
-                "overflow": "hidden",
-                "margin_top": "1px",
-                "&:first-child": {
-                    "margin_top": 0,
-                    "border_top_left_radius": "4px",
-                    "border_top_right_radius": "4px",
-                },
-                "&:last-child": {
-                    "border_bottom_left_radius": "4px",
-                    "border_bottom_right_radius": "4px",
-                },
-                "&:focus-within": {
-                    "position": "relative",
-                    "z_index": 1,
-                    "box_shadow": "0 0 0 2px var(--accent-7)",
-                },
                 **self.style,
             }
         )
@@ -109,7 +504,6 @@ class AccordionHeader(AccordionComponent):
     def _apply_theme(self, theme: Component):
         self.style = Style(
             {
-                "display": "flex",
                 **self.style,
             }
         )
@@ -125,27 +519,6 @@ class AccordionTrigger(AccordionComponent):
     def _apply_theme(self, theme: Component):
         self.style = Style(
             {
-                "font_family": "inherit",
-                "padding": "0 20px",
-                "height": "45px",
-                "flex": 1,
-                "display": "flex",
-                "align_items": "center",
-                "justify_content": "space-between",
-                "font_size": "15px",
-                "line_height": 1,
-                "color": "var(--accent-11)",
-                "box_shadow": "0 1px 0 var(--accent-6)",
-                "&:hover": {
-                    "background_color": "var(--gray-2)",
-                },
-                "& > .AccordionChevron": {
-                    "color": "var(--accent-10)",
-                    "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
-                },
-                "&[data-state='open'] > .AccordionChevron": {
-                    "transform": "rotate(180deg)",
-                },
                 **self.style,
             }
         )
@@ -161,23 +534,6 @@ class AccordionContent(AccordionComponent):
     def _apply_theme(self, theme: Component):
         self.style = Style(
             {
-                "overflow": "hidden",
-                "fontSize": "15px",
-                "color": "var(--accent-11)",
-                "backgroundColor": "var(--accent-2)",
-                "padding": "15px, 20px",
-                "&[data-state='open']": {
-                    "animation": Var.create(
-                        f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
-                        _var_is_string=True,
-                    ),
-                },
-                "&[data-state='closed']": {
-                    "animation": Var.create(
-                        f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms cubic-bezier(0.87, 0, 0.13, 1)",
-                        _var_is_string=True,
-                    ),
-                },
                 **self.style,
             }
         )
@@ -231,14 +587,14 @@ def accordion_item(header: Component, content: Component, **props) -> Component:
                     tag="chevron_down",
                     class_name="AccordionChevron",
                 ),
+                class_name="AccordionTrigger",
             ),
         ),
         AccordionContent.create(
             content,
+            class_name="AccordionContent",
         ),
         value=value,
         **props,
+        class_name="AccordionItem",
     )
-
-
-accordion = AccordionRoot.create

+ 41 - 11
reflex/components/radix/primitives/accordion.pyi

@@ -7,19 +7,37 @@ 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 typing import Any, Dict, Literal
+from reflex.components.base.fragment import Fragment
 from reflex.components.component import Component
+from reflex.components.core import cond, match
 from reflex.components.radix.primitives.base import RadixPrimitiveComponent
 from reflex.components.radix.themes.components.icons import Icon
-from reflex.style import Style
+from reflex.style import (
+    Style,
+    convert_dict_to_style_and_format_emotion,
+    format_as_emotion,
+)
 from reflex.utils import imports
-from reflex.vars import Var
+from reflex.vars import BaseVar, Var
 
 LiteralAccordionType = Literal["single", "multiple"]
 LiteralAccordionDir = Literal["ltr", "rtl"]
 LiteralAccordionOrientation = Literal["vertical", "horizontal"]
+LiteralAccordionRootVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
+LiteralAccordionRootColorScheme = Literal["primary", "accent"]
 DEFAULT_ANIMATION_DURATION = 250
 
+def get_theme_accordion_root(variant: Var[str], color_scheme: Var[str]) -> BaseVar: ...
+def get_theme_accordion_item(): ...
+def get_theme_accordion_header() -> dict[str, str]: ...
+def get_theme_accordion_trigger(
+    variant: str | Var, color_scheme: str | Var
+) -> BaseVar: ...
+def get_theme_accordion_content(
+    variant: str | Var, color_scheme: str | Var
+) -> BaseVar: ...
+
 class AccordionComponent(RadixPrimitiveComponent):
     @overload
     @classmethod
@@ -121,6 +139,16 @@ class AccordionRoot(AccordionComponent):
                 Literal["vertical", "horizontal"],
             ]
         ] = None,
+        variant: Optional[
+            Union[
+                Var[Literal["classic", "soft", "surface", "outline", "ghost"]],
+                Literal["classic", "soft", "surface", "outline", "ghost"],
+            ]
+        ] = None,
+        color_scheme: Optional[
+            Union[Var[Literal["primary", "accent"]], Literal["primary", "accent"]]
+        ] = None,
+        _dynamic_themes: Optional[Union[Var[dict], dict]] = None,
         as_child: Optional[Union[Var[bool], bool]] = None,
         style: Optional[Style] = None,
         key: Optional[Any] = None,
@@ -173,9 +201,12 @@ class AccordionRoot(AccordionComponent):
         on_unmount: Optional[
             Union[EventHandler, EventSpec, list, function, BaseVar]
         ] = None,
+        on_value_change: Optional[
+            Union[EventHandler, EventSpec, list, function, BaseVar]
+        ] = None,
         **props
     ) -> "AccordionRoot":
-        """Create the component.
+        """Create the Accordion root component.
 
         Args:
             *children: The children of the component.
@@ -186,6 +217,9 @@ class AccordionRoot(AccordionComponent):
             disabled: Whether or not the accordion is disabled.
             dir: The reading direction of the accordion when applicable.
             orientation: The orientation of the accordion.
+            variant: The variant of the accordion.
+            color_scheme: The color scheme of the accordion.
+            _dynamic_themes: dynamic themes of the accordion generated at compile time.
             as_child: Change the default rendered element for the one passed as a child.
             style: The style of the component.
             key: A unique key for the component.
@@ -193,15 +227,13 @@ class AccordionRoot(AccordionComponent):
             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.
+            **props: The properties of the component.
 
         Returns:
-            The component.
-
-        Raises:
-            TypeError: If an invalid child is passed.
+            The Accordion root Component.
         """
         ...
+    def get_event_triggers(self) -> Dict[str, Any]: ...
 
 class AccordionItem(AccordionComponent):
     @overload
@@ -532,5 +564,3 @@ class AccordionContent(AccordionComponent):
         ...
 
 def accordion_item(header: Component, content: Component, **props) -> Component: ...
-
-accordion = AccordionRoot.create

+ 4 - 0
reflex/components/radix/primitives/base.py

@@ -15,6 +15,10 @@ class RadixPrimitiveComponent(Component):
 
     lib_dependencies: List[str] = ["@emotion/react@^11.11.1"]
 
+
+class RadixPrimitiveComponentWithClassName(RadixPrimitiveComponent):
+    """Basic component for radix Primitives with a class name prop."""
+
     def _render(self) -> Tag:
         return (
             super()

+ 81 - 0
reflex/components/radix/primitives/base.pyi

@@ -93,3 +93,84 @@ class RadixPrimitiveComponent(Component):
             TypeError: If an invalid child is passed.
         """
         ...
+
+class RadixPrimitiveComponentWithClassName(RadixPrimitiveComponent):
+    @overload
+    @classmethod
+    def create(  # type: ignore
+        cls,
+        *children,
+        as_child: 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
+    ) -> "RadixPrimitiveComponentWithClassName":
+        """Create the component.
+
+        Args:
+            *children: The children of the component.
+            as_child: Change the default rendered element for the one passed as a child.
+            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.
+
+        Raises:
+            TypeError: If an invalid child is passed.
+        """
+        ...

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

@@ -14,7 +14,7 @@ from reflex.utils import imports
 from reflex.utils.format import format_event_chain, to_camel_case
 from reflex.vars import BaseVar, Var
 
-from .base import RadixPrimitiveComponent
+from .base import RadixPrimitiveComponentWithClassName
 
 FORM_DATA = Var.create("form_data")
 HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
@@ -34,7 +34,7 @@ HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
 )
 
 
-class FormComponent(RadixPrimitiveComponent):
+class FormComponent(RadixPrimitiveComponentWithClassName):
     """Base class for all @radix-ui/react-form components."""
 
     library = "@radix-ui/react-form@^0.0.3"

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

@@ -18,14 +18,14 @@ from reflex.event import EventChain
 from reflex.utils import imports
 from reflex.utils.format import format_event_chain, to_camel_case
 from reflex.vars import BaseVar, Var
-from .base import RadixPrimitiveComponent
+from .base import RadixPrimitiveComponentWithClassName
 
 FORM_DATA = Var.create("form_data")
 HANDLE_SUBMIT_JS_JINJA2 = Environment().from_string(
     "\n    const handleSubmit_{{ handle_submit_unique_name }} = useCallback((ev) => {\n        const $form = ev.target\n        ev.preventDefault()\n        const {{ form_data }} = {...Object.fromEntries(new FormData($form).entries()), ...{{ field_ref_mapping }}}\n\n        {{ on_submit_event_chain }}\n\n        if ({{ reset_on_submit }}) {\n            $form.reset()\n        }\n    })\n    "
 )
 
-class FormComponent(RadixPrimitiveComponent):
+class FormComponent(RadixPrimitiveComponentWithClassName):
     @overload
     @classmethod
     def create(  # type: ignore

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

@@ -3,12 +3,12 @@
 from typing import Optional
 
 from reflex.components.component import Component
-from reflex.components.radix.primitives.base import RadixPrimitiveComponent
+from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
 from reflex.style import Style
 from reflex.vars import Var
 
 
-class ProgressComponent(RadixPrimitiveComponent):
+class ProgressComponent(RadixPrimitiveComponentWithClassName):
     """A Progress component."""
 
     library = "@radix-ui/react-progress@^1.0.3"

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

@@ -9,11 +9,11 @@ from reflex.event import EventChain, EventHandler, EventSpec
 from reflex.style import Style
 from typing import Optional
 from reflex.components.component import Component
-from reflex.components.radix.primitives.base import RadixPrimitiveComponent
+from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
 from reflex.style import Style
 from reflex.vars import Var
 
-class ProgressComponent(RadixPrimitiveComponent):
+class ProgressComponent(RadixPrimitiveComponentWithClassName):
     @overload
     @classmethod
     def create(  # type: ignore

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

@@ -3,7 +3,7 @@
 from typing import Any, Dict, Literal
 
 from reflex.components.component import Component
-from reflex.components.radix.primitives.base import RadixPrimitiveComponent
+from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
 from reflex.style import Style
 from reflex.vars import Var
 
@@ -11,7 +11,7 @@ LiteralSliderOrientation = Literal["horizontal", "vertical"]
 LiteralSliderDir = Literal["ltr", "rtl"]
 
 
-class SliderComponent(RadixPrimitiveComponent):
+class SliderComponent(RadixPrimitiveComponentWithClassName):
     """Base class for all @radix-ui/react-slider components."""
 
     library = "@radix-ui/react-slider@^1.1.2"

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

@@ -9,14 +9,14 @@ from reflex.event import EventChain, EventHandler, EventSpec
 from reflex.style import Style
 from typing import Any, Dict, Literal
 from reflex.components.component import Component
-from reflex.components.radix.primitives.base import RadixPrimitiveComponent
+from reflex.components.radix.primitives.base import RadixPrimitiveComponentWithClassName
 from reflex.style import Style
 from reflex.vars import Var
 
 LiteralSliderOrientation = Literal["horizontal", "vertical"]
 LiteralSliderDir = Literal["ltr", "rtl"]
 
-class SliderComponent(RadixPrimitiveComponent):
+class SliderComponent(RadixPrimitiveComponentWithClassName):
     @overload
     @classmethod
     def create(  # type: ignore

+ 1 - 0
reflex/components/radix/themes/components/icons.py

@@ -46,6 +46,7 @@ class Icon(RadixIconComponent):
                 f"Invalid icon tag: {props['tag']}. Please use one of the following: {sorted(ICON_LIST)}"
             )
         props["tag"] = format.to_title_case(props["tag"]) + "Icon"
+        props["alias"] = f"RadixThemes{props['tag']}"
         return super().create(*children, **props)
 
 

+ 15 - 0
reflex/style.py

@@ -220,3 +220,18 @@ def format_as_emotion(style_dict: dict[str, Any]) -> dict[str, Any] | None:
             emotion_style[key] = value
     if emotion_style:
         return emotion_style
+
+
+def convert_dict_to_style_and_format_emotion(
+    raw_dict: dict[str, Any]
+) -> dict[str, Any] | None:
+    """Convert a dict to a style dict and then format as emotion.
+
+    Args:
+        raw_dict: The dict to convert.
+
+    Returns:
+        The emotion dict.
+
+    """
+    return format_as_emotion(Style(raw_dict))

+ 37 - 3
reflex/vars.py

@@ -421,6 +421,26 @@ class Var:
             and self._var_data == other._var_data
         )
 
+    def _merge(self, other) -> Var:
+        """Merge two or more dicts.
+
+        Args:
+            other: The other var to merge.
+
+        Returns:
+            The merged var.
+
+        Raises:
+            ValueError: If the other value to be merged is None.
+        """
+        if other is None:
+            raise ValueError("The value to be merged cannot be None.")
+        if not isinstance(other, Var):
+            other = Var.create(other)
+        return self._replace(
+            _var_name=f"{{...{self._var_name}, ...{other._var_name}}}"  # type: ignore
+        )
+
     def to_string(self, json: bool = True) -> Var:
         """Convert a var to a string.
 
@@ -677,6 +697,16 @@ class Var:
 
         left_operand, right_operand = (other, self) if flip else (self, other)
 
+        def get_operand_full_name(operand):
+            # operand vars that are string literals need to be wrapped in back ticks.
+            return (
+                operand._var_name_unwrapped
+                if operand._var_is_string
+                and not operand._var_state
+                and operand._var_is_local
+                else operand._var_full_name
+            )
+
         if other is not None:
             # check if the operation between operands is valid.
             if op and not self.is_valid_operation(
@@ -688,18 +718,22 @@ class Var:
                     f"Unsupported Operand type(s) for {op}: `{left_operand._var_full_name}` of type {left_operand._var_type.__name__} and `{right_operand._var_full_name}` of type {right_operand._var_type.__name__}"  # type: ignore
                 )
 
+            left_operand_full_name = get_operand_full_name(left_operand)
+            right_operand_full_name = get_operand_full_name(right_operand)
+
             # apply function to operands
             if fn is not None:
+
                 if invoke_fn:
                     # invoke the function on left operand.
-                    operation_name = f"{left_operand._var_full_name}.{fn}({right_operand._var_full_name})"  # type: ignore
+                    operation_name = f"{left_operand_full_name}.{fn}({right_operand_full_name})"  # type: ignore
                 else:
                     # pass the operands as arguments to the function.
-                    operation_name = f"{left_operand._var_full_name} {op} {right_operand._var_full_name}"  # type: ignore
+                    operation_name = f"{left_operand_full_name} {op} {right_operand_full_name}"  # type: ignore
                     operation_name = f"{fn}({operation_name})"
             else:
                 # apply operator to operands (left operand <operator> right_operand)
-                operation_name = f"{left_operand._var_full_name} {op} {right_operand._var_full_name}"  # type: ignore
+                operation_name = f"{left_operand_full_name} {op} {right_operand_full_name}"  # type: ignore
                 operation_name = format.wrap(operation_name, "(")
         else:
             # apply operator to left operand (<operator> left_operand)

+ 35 - 0
tests/test_var.py

@@ -262,6 +262,41 @@ def test_basic_operations(TestObj):
     assert str(v(1) | v(2)) == "{(1 || 2)}"
     assert str(v([1, 2, 3])[v(0)]) == "{[1, 2, 3].at(0)}"
     assert str(v({"a": 1, "b": 2})["a"]) == '{{"a": 1, "b": 2}["a"]}'
+    assert str(v("foo") == v("bar")) == '{("foo" === "bar")}'
+    assert (
+        str(
+            Var.create("foo", _var_is_local=False)
+            == Var.create("bar", _var_is_local=False)
+        )
+        == "{(foo === bar)}"
+    )
+    assert (
+        str(
+            BaseVar(
+                _var_name="foo", _var_type=str, _var_is_string=True, _var_is_local=True
+            )
+            == BaseVar(
+                _var_name="bar", _var_type=str, _var_is_string=True, _var_is_local=True
+            )
+        )
+        == "(`foo` === `bar`)"
+    )
+    assert (
+        str(
+            BaseVar(
+                _var_name="foo",
+                _var_type=TestObj,
+                _var_is_string=True,
+                _var_is_local=False,
+            )
+            ._var_set_state("state")
+            .bar
+            == BaseVar(
+                _var_name="bar", _var_type=str, _var_is_string=True, _var_is_local=True
+            )
+        )
+        == "{(state.foo.bar === `bar`)}"
+    )
     assert (
         str(BaseVar(_var_name="foo", _var_type=TestObj)._var_set_state("state").bar)
         == "{state.foo.bar}"