diff --git a/reflex/app.py b/reflex/app.py index 754541545..c6cf8af80 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -833,9 +833,7 @@ class App(Base): for _route, component in self.pages.items(): # Merge the component style with the app style. - component._add_style_recursive(self.style) - - component.apply_theme(self.theme) + component._add_style_recursive(self.style, self.theme) # Add component._get_all_imports() to all_imports. all_imports.update(component._get_all_imports()) diff --git a/reflex/compiler/compiler.py b/reflex/compiler/compiler.py index 9000b54f9..3f4f9a4be 100644 --- a/reflex/compiler/compiler.py +++ b/reflex/compiler/compiler.py @@ -263,9 +263,18 @@ def _compile_stateful_components( # Reset this flag to render the actual component. component.rendered_as_shared = False + # Include dynamic imports in the shared component. + if dynamic_imports := component._get_all_dynamic_imports(): + rendered_components.update( + {dynamic_import: None for dynamic_import in dynamic_imports} + ) + + # Include custom code in the shared component. rendered_components.update( {code: None for code in component._get_all_custom_code()}, ) + + # Include all imports in the shared component. all_import_dicts.append(component._get_all_imports()) # Indicate that this component now imports from the shared file. diff --git a/reflex/components/component.py b/reflex/components/component.py index 463cc9c56..ab69e3716 100644 --- a/reflex/components/component.py +++ b/reflex/components/component.py @@ -608,6 +608,8 @@ class Component(BaseComponent, ABC): def _apply_theme(self, theme: Optional[Component]): """Apply the theme to this component. + Deprecated. Use add_style instead. + Args: theme: The theme to apply. """ @@ -779,44 +781,119 @@ class Component(BaseComponent, ABC): return cls(children=children, **props) - def _add_style(self, style: dict): - """Add additional style to the component. + def add_style(self) -> Style | None: + """Add style to the component. + + Downstream components can override this method to return a style dict + that will be applied to the component. + + Returns: + The style to add. + """ + return None + + def _add_style(self) -> Style: + """Call add_style for all bases in the MRO. + + Downstream components should NOT override. Use add_style instead. + + Returns: + The style to add. + """ + styles = [] + vars = [] + + # Walk the MRO to call all `add_style` methods. + for base in self._iter_parent_classes_with_method("add_style"): + s = base.add_style(self) # type: ignore + if s is not None: + styles.append(s) + vars.append(s._var_data) + + _style = Style() + for s in reversed(styles): + _style.update(s) + + _style._var_data = VarData.merge(*vars) + return _style + + def _get_component_style(self, styles: ComponentStyle) -> Style | None: + """Get the style to the component from `App.style`. Args: - style: A style dict to apply. - """ - self.style.update(style) + styles: The style to apply. - def _add_style_recursive(self, style: ComponentStyle) -> Component: + Returns: + The style of the component. + """ + component_style = None + if type(self) in styles: + component_style = Style(styles[type(self)]) + if self.create in styles: + component_style = Style(styles[self.create]) + return component_style + + def _add_style_recursive( + self, style: ComponentStyle, theme: Optional[Component] = None + ) -> Component: """Add additional style to the component and its children. + Apply order is as follows (with the latest overriding the earliest): + 1. Default style from `_add_style`/`add_style`. + 2. User-defined style from `App.style`. + 3. User-defined style from `Component.style`. + 4. style dict and css props passed to the component instance. + Args: style: A dict from component to styling. + theme: The theme to apply. (for retro-compatibility with deprecated _apply_theme API) + + Raises: + UserWarning: If `_add_style` has been overridden. Returns: The component with the additional style. """ - component_style = None - if type(self) in style: - # Extract the style for this component. - component_style = Style(style[type(self)]) - if self.create in style: - component_style = Style(style[self.create]) - if component_style is not None: - # Only add style props that are not overridden. - component_style = { - k: v for k, v in component_style.items() if k not in self.style - } + # 1. Default style from `_add_style`/`add_style`. + if type(self)._add_style != Component._add_style: + raise UserWarning( + "Do not override _add_style directly. Use add_style instead." + ) + new_style = self._add_style() + style_vars = [new_style._var_data] - # Add the style to the component. - self._add_style(component_style) + # 2. User-defined style from `App.style`. + component_style = self._get_component_style(style) + if component_style: + new_style.update(component_style) + style_vars.append(component_style._var_data) + + # 3. User-defined style from `Component.style`. + # Apply theme for retro-compatibility with deprecated _apply_theme API + if type(self)._apply_theme != Component._apply_theme: + console.deprecate( + f"{self.__class__.__name__}._apply_theme", + reason="use add_style instead", + deprecation_version="0.5.0", + removal_version="0.6.0", + ) + self._apply_theme(theme) + + # 4. style dict and css props passed to the component instance. + new_style.update(self.style) + style_vars.append(self.style._var_data) + + new_style._var_data = VarData.merge(*style_vars) + + # Assign the new style + self.style = new_style # Recursively add style to the children. for child in self.children: # Skip BaseComponent and StatefulComponent children. if not isinstance(child, Component): continue - child._add_style_recursive(style) + child._add_style_recursive(style, theme) return self def _get_style(self) -> dict: diff --git a/reflex/components/core/cond.py b/reflex/components/core/cond.py index 737b650d8..0e3e43672 100644 --- a/reflex/components/core/cond.py +++ b/reflex/components/core/cond.py @@ -102,15 +102,6 @@ class Cond(MemoizationLeaf): _IS_TRUE_IMPORT, ) - def _apply_theme(self, theme: Component): - """Apply the theme to this component. - - Args: - theme: The theme to apply. - """ - self.comp1.apply_theme(theme) # type: ignore - self.comp2.apply_theme(theme) # type: ignore - @overload def cond(condition: Any, c1: Component, c2: Any) -> Component: diff --git a/reflex/components/core/foreach.py b/reflex/components/core/foreach.py index 0ef7aeefa..8bca0a3db 100644 --- a/reflex/components/core/foreach.py +++ b/reflex/components/core/foreach.py @@ -3,7 +3,7 @@ from __future__ import annotations import inspect from hashlib import md5 -from typing import Any, Callable, Iterable, Optional +from typing import Any, Callable, Iterable from reflex.components.base.fragment import Fragment from reflex.components.component import Component @@ -23,17 +23,6 @@ class Foreach(Component): # A function from the render args to the component. render_fn: Callable = Fragment.create - # The theme if set. - theme: Optional[Component] = None - - def _apply_theme(self, theme: Component): - """Apply the theme to this component. - - Args: - theme: The theme to apply. - """ - self.theme = theme - @classmethod def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach: """Create a foreach component. @@ -97,10 +86,6 @@ class Foreach(Component): tag = self._render() component = tag.render_component() - # Apply the theme to the children. - if self.theme is not None: - component.apply_theme(self.theme) - return dict( tag.add_props( **self.event_triggers, diff --git a/reflex/components/core/match.py b/reflex/components/core/match.py index 169efe334..97741b9fa 100644 --- a/reflex/components/core/match.py +++ b/reflex/components/core/match.py @@ -273,18 +273,3 @@ class Match(MemoizationLeaf): super()._get_imports(), getattr(self.cond._var_data, "imports", {}), ) - - def _apply_theme(self, theme: Component): - """Apply the theme to this component. - - Args: - theme: The theme to apply. - """ - # apply theme to return components. - for match_case in self.match_cases: - if isinstance(match_case[-1], Component): - match_case[-1].apply_theme(theme) - - # apply theme to default component - if isinstance(self.default, Component): - self.default.apply_theme(theme) diff --git a/reflex/components/datadisplay/code.py b/reflex/components/datadisplay/code.py index f19401c3f..c59256b07 100644 --- a/reflex/components/datadisplay/code.py +++ b/reflex/components/datadisplay/code.py @@ -1,4 +1,6 @@ """A code component.""" +from __future__ import annotations + import re from typing import Dict, Literal, Optional, Union @@ -491,8 +493,9 @@ class CodeBlock(Component): else: return code_block - def _add_style(self, style): - self.custom_style.update(style) # type: ignore + def add_style(self) -> Style | None: + """Add style to the component.""" + self.custom_style.update(self.style) def _render(self): out = super()._render() diff --git a/reflex/components/datadisplay/code.pyi b/reflex/components/datadisplay/code.pyi index 1fc40dbce..f3aeb8e18 100644 --- a/reflex/components/datadisplay/code.pyi +++ b/reflex/components/datadisplay/code.pyi @@ -1111,5 +1111,6 @@ class CodeBlock(Component): The text component. """ ... + def add_style(self) -> Style | None: ... @staticmethod def convert_theme_name(theme) -> str: ... diff --git a/reflex/components/markdown/markdown.py b/reflex/components/markdown/markdown.py index ca54d0aa5..5ae712fc2 100644 --- a/reflex/components/markdown/markdown.py +++ b/reflex/components/markdown/markdown.py @@ -18,7 +18,6 @@ from reflex.components.radix.themes.typography.heading import Heading from reflex.components.radix.themes.typography.link import Link from reflex.components.radix.themes.typography.text import Text from reflex.components.tags.tag import Tag -from reflex.style import Style from reflex.utils import console, imports, types from reflex.utils.imports import ImportVar from reflex.vars import Var @@ -230,7 +229,8 @@ class Markdown(Component): component = self.component_map[tag](*children, **props).set( special_props=special_props ) - component._add_style(Style(self.custom_styles.get(tag, {}))) + component.style.update(self.custom_styles.get(tag, {})) + return component def format_component(self, tag: str, **props) -> str: diff --git a/reflex/components/markdown/markdown.pyi b/reflex/components/markdown/markdown.pyi index 85ffeeeed..fbb830201 100644 --- a/reflex/components/markdown/markdown.pyi +++ b/reflex/components/markdown/markdown.pyi @@ -22,7 +22,6 @@ from reflex.components.radix.themes.typography.heading import Heading from reflex.components.radix.themes.typography.link import Link from reflex.components.radix.themes.typography.text import Text from reflex.components.tags.tag import Tag -from reflex.style import Style from reflex.utils import console, imports, types from reflex.utils.imports import ImportVar from reflex.vars import Var diff --git a/reflex/components/radix/primitives/accordion.py b/reflex/components/radix/primitives/accordion.py index 40f2d3495..529c9efcb 100644 --- a/reflex/components/radix/primitives/accordion.py +++ b/reflex/components/radix/primitives/accordion.py @@ -5,303 +5,43 @@ from __future__ import annotations from typing import Any, Dict, List, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace -from reflex.components.core.match import Match +from reflex.components.core.colors import color from reflex.components.lucide.icon import Icon from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import LiteralAccentColor -from reflex.style import ( - Style, - convert_dict_to_style_and_format_emotion, - format_as_emotion, -) +from reflex.style import Style from reflex.utils import imports -from reflex.vars import BaseVar, Var, VarData, get_uuid_string_var +from reflex.vars import Var, get_uuid_string_var LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionDir = Literal["ltr", "rtl"] LiteralAccordionOrientation = Literal["vertical", "horizontal"] -LiteralAccordionRootVariant = Literal["classic", "soft", "surface", "outline", "ghost"] -LiteralAccordionRootColorScheme = Literal["primary", "accent"] +LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"] 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.create( # type: ignore - variant, - ( - "soft", - convert_dict_to_style_and_format_emotion( - { - "border_radius": "6px", - "background_color": f"var(--{color_scheme}-3)", - "box_shadow": "0 2px 10px var(--black-a1)", - } - ), - ), - ( - "outline", - convert_dict_to_style_and_format_emotion( - { - "border_radius": "6px", - "border": f"1px solid var(--{color_scheme}-6)", - "box_shadow": "0 2px 10px var(--black-a1)", - } - ), - ), - ( - "surface", - convert_dict_to_style_and_format_emotion( - { - "border_radius": "6px", - "border": f"1px solid var(--{color_scheme}-6)", - "background_color": f"var(--{color_scheme}-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": f"var(--{color_scheme}-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", - "&: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.create( # type: ignore - variant, - ( - "soft", - convert_dict_to_style_and_format_emotion( - { - "color": f"var(--{color_scheme}-11)", - "&:hover": { - "background_color": f"var(--{color_scheme}-4)", - }, - "& > .AccordionChevron": { - "color": f"var(--{color_scheme}-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", - "line_height": 1, - } - ), - ), - ( - "outline", - "surface", - "ghost", - convert_dict_to_style_and_format_emotion( - { - "color": f"var(--{color_scheme}-11)", - "&:hover": { - "background_color": f"var(--{color_scheme}-4)", - }, - "& > .AccordionChevron": { - "color": f"var(--{color_scheme}-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", - "line_height": 1, - } - ), - ), - # defaults to classic - convert_dict_to_style_and_format_emotion( - { - "color": f"var(--{color_scheme}-9-contrast)", - "box_shadow": f"var(--{color_scheme}-11)", - "&:hover": { - "background_color": f"var(--{color_scheme}-10)", - }, - "& > .AccordionChevron": { - "color": f"var(--{color_scheme}-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.create( # type: ignore - variant, - ( - "outline", - "ghost", - convert_dict_to_style_and_format_emotion( - { - "overflow": "hidden", - "font_size": "10px", - "color": f"var(--{color_scheme}-11)", - "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": Match.create( - variant, - ("classic", f"var(--{color_scheme}-9-contrast)"), - f"var(--{color_scheme}-11)", - ), - "background_color": Match.create( - variant, - ("classic", f"var(--{color_scheme}-9)"), - f"var(--{color_scheme}-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.""" library = "@radix-ui/react-accordion@^1.1.2" + # The color scheme of the component. + color_scheme: Var[LiteralAccentColor] + + # The variant of the component. + variant: Var[LiteralAccordionVariant] = Var.create_safe("classic") + + def add_style(self) -> Style | None: + """Add style to the component.""" + if self.color_scheme is not None: + self.custom_attrs["data-accent-color"] = self.color_scheme + + self.custom_attrs["data-variant"] = self.variant + + def _exclude_props(self) -> list[str]: + return ["color_scheme", "variant"] + class AccordionRoot(AccordionComponent): """An accordion component.""" @@ -332,16 +72,7 @@ class AccordionRoot(AccordionComponent): orientation: Var[LiteralAccordionOrientation] # The variant of the accordion. - variant: Var[LiteralAccordionRootVariant] = "classic" # type: ignore - - # The color scheme of the accordion. - color_scheme: Var[LiteralAccentColor] # type: ignore - - # dynamic themes of the accordion generated at compile time. - _dynamic_themes: Var[dict] = Var.create({}) # type: ignore - - # The var_data associated with the component. - _var_data: VarData = VarData() # type: ignore + variant: Var[LiteralAccordionVariant] = Var.create_safe("classic") _valid_children: List[str] = ["AccordionItem"] @@ -356,81 +87,12 @@ class AccordionRoot(AccordionComponent): Returns: The Accordion root Component. """ - comp = super().create(*children, **props) + for child in children: + if isinstance(child, AccordionItem): + child.color_scheme = props.get("color_scheme") # type: ignore + child.variant = props.get("variant") # type: ignore - if comp.color_scheme is not None and 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 comp.variant is not None and 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 - - return comp - - 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): - global_color_scheme = getattr(theme, "accent_color", None) - - if global_color_scheme is None and self.color_scheme is None: - raise ValueError( - "`color_scheme` cannot be None. Either set the `color_scheme` prop on the accordion " - "component or set the `accent_color` prop in your global theme." - ) - - # prepare the color_scheme var to be used in an f-string(strip off the wrapping curly brace) - color_scheme = Var.create( - self.color_scheme if self.color_scheme is not None else global_color_scheme - )._replace( # type: ignore - _var_is_string=False - ) - - accordion_theme_root = get_theme_accordion_root( - variant=self.variant, color_scheme=color_scheme - ) - accordion_theme_content = get_theme_accordion_content( - variant=self.variant, color_scheme=color_scheme - ) - accordion_theme_trigger = get_theme_accordion_trigger( - variant=self.variant, color_scheme=color_scheme - ) - - # extract var_data from dynamic themes. - self._var_data = ( - self._var_data.merge( # type: ignore - accordion_theme_trigger._var_data, - accordion_theme_content._var_data, - accordion_theme_root._var_data, - ) - or self._var_data - ) - - 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": accordion_theme_trigger, - "& .AccordionContent": accordion_theme_content, - } - ) - )._merge( # type: ignore - accordion_theme_root - ) - - def _get_imports(self): - return imports.merge_imports( - super()._get_imports(), - self._var_data.imports if self._var_data else {}, - {"@emotion/react": [imports.ImportVar(tag="keyframes")]}, - ) + return super().create(*children, **props) def get_event_triggers(self) -> Dict[str, Any]: """Get the events triggers signatures for the component. @@ -443,28 +105,36 @@ class AccordionRoot(AccordionComponent): "on_value_change": lambda e0: [e0], } - def _get_custom_code(self) -> str: - return """ -const slideDown = keyframes` -from { - height: 0; -} -to { - height: var(--radix-accordion-content-height); -} -` -const slideUp = keyframes` -from { - height: var(--radix-accordion-content-height); -} -to { - height: 0; -} -` -""" + def add_style(self): + """Add style to the component. - def _exclude_props(self) -> list[str]: - return ["color_scheme", "variant"] + Returns: + The style of the component. + """ + return Style( + { + "border_radius": "6px", + "box_shadow": f"0 2px 10px {color('black', 1, alpha=True)}", + "&[data-variant='classic']": { + "background_color": color("accent", 9), + "box_shadow": f"0 2px 10px {color('black', 4, alpha=True)}", + }, + "&[data-variant='soft']": { + "background_color": color("accent", 3), + }, + "&[data-variant='outline']": { + "border": f"1px solid {color('accent', 6)}", + }, + "&[data-variant='surface']": { + "border": f"1px solid {color('accent', 6)}", + "background_color": color("accent", 3), + }, + "&[data-variant='ghost']": { + "background_color": "none", + "box_shadow": "None", + }, + } + ) class AccordionItem(AccordionComponent): @@ -488,13 +158,6 @@ class AccordionItem(AccordionComponent): _valid_parents: List[str] = ["AccordionRoot"] - def _apply_theme(self, theme: Component): - self.style = Style( - { - **self.style, - } - ) - @classmethod def create( cls, @@ -506,9 +169,9 @@ class AccordionItem(AccordionComponent): """Create an accordion item. Args: + *children: The list of children to use if header and content are not provided. header: The header of the accordion item. content: The content of the accordion item. - *children: The list of children to use if header and content are not provided. **props: Additional properties to apply to the accordion item. Returns: @@ -527,14 +190,55 @@ class AccordionItem(AccordionComponent): AccordionHeader.create( AccordionTrigger.create( header, - AccordionIcon.create(), + AccordionIcon.create( + color_scheme=props.get("color_scheme"), + variant=props.get("variant"), + ), + color_scheme=props.get("color_scheme"), + variant=props.get("variant"), ), + color_scheme=props.get("color_scheme"), + variant=props.get("variant"), + ), + AccordionContent.create( + content, color_scheme=props.get("color_scheme") ), - AccordionContent.create(content), ] return super().create(*children, value=value, **props, class_name=cls_name) + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + for child in self.children: + if isinstance(child, (AccordionHeader, AccordionContent)): + child.color_scheme = self.color_scheme + child.variant = self.variant + + return Style( + { + "overflow": "hidden", + "width": "100%", + "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, + }, + } + ) + class AccordionHeader(AccordionComponent): """An accordion component.""" @@ -561,8 +265,21 @@ class AccordionHeader(AccordionComponent): return super().create(*children, class_name=cls_name, **props) - def _apply_theme(self, theme: Component): - self.style = Style({**self.style}) + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + for child in self.children: + if isinstance(child, AccordionTrigger): + child.color_scheme = self.color_scheme + child.variant = self.variant + + return Style({"display": "flex"}) + + +cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)" class AccordionTrigger(AccordionComponent): @@ -590,8 +307,52 @@ class AccordionTrigger(AccordionComponent): return super().create(*children, class_name=cls_name, **props) - def _apply_theme(self, theme: Component): - self.style = Style({**self.style}) + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + for child in self.children: + if isinstance(child, AccordionIcon): + child.color_scheme = self.color_scheme + child.variant = self.variant + + return Style( + { + "color": color("accent", 11), + "line_height": 1, + "font_size": "15px", + "justify_content": "space-between", + "align_items": "center", + "flex": 1, + "display": "flex", + "padding": "0 20px", + "height": "45px", + "font_family": "inherit", + "width": "100%", + "&[data-state='open'] > .AccordionChevron": { + "transform": "rotate(180deg)", + }, + "&:hover": { + "background_color": color("accent", 4), + }, + "& > .AccordionChevron": { + "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}", + }, + "&[data-variant='classic']": { + "color": color("accent", 12), + "box_shadow": color("accent", 11), + "&:hover": { + "background_color": color("accent", 10), + }, + "& > .AccordionChevron": { + "color": color("accent", 12), + "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}", + }, + }, + } + ) class AccordionIcon(Icon): @@ -623,6 +384,14 @@ class AccordionContent(AccordionComponent): alias = "RadixAccordionContent" + def add_imports(self) -> imports.ImportDict: + """Add imports to the component. + + Returns: + The imports of the component. + """ + return {"@emotion/react": [imports.ImportVar(tag="keyframes")]} + @classmethod def create(cls, *children, **props) -> Component: """Create the Accordion content component. @@ -641,14 +410,66 @@ class AccordionContent(AccordionComponent): return super().create(*children, class_name=cls_name, **props) - def _apply_theme(self, theme: Component): - self.style = Style({**self.style}) + def add_custom_code(self) -> list[str]: + """Add custom code to the component. - # def _get_imports(self): - # return { - # **super()._get_imports(), - # "@emotion/react": [imports.ImportVar(tag="keyframes")], - # } + Returns: + The custom code of the component. + """ + return [ + """ +const slideDown = keyframes` +from { + height: 0; +} +to { + height: var(--radix-accordion-content-height); +} +` +const slideUp = keyframes` +from { + height: var(--radix-accordion-content-height); +} +to { + height: 0; +} +` +""" + ] + + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + slideDown = Var.create( + f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}", + _var_is_string=True, + ) + + slideUp = Var.create( + f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}", + _var_is_string=True, + ) + + return Style( + { + "overflow": "hidden", + "font_size": "10px", + "color": color("accent", 11), + "background_color": color("accent", 3), + "padding": "0 15px", + "&[data-state='open']": {"animation": slideDown}, + "&[data-state='closed']": {"animation": slideUp}, + "&[data-variant='classic']": { + "color": color("accent", 12), + "background_color": color("accent", 9), + }, + "&[data-variant='outline']": {"background_color": "transparent"}, + "&[data-variant='ghost']": {"background_color": "transparent"}, + } + ) class Accordion(ComponentNamespace): diff --git a/reflex/components/radix/primitives/accordion.pyi b/reflex/components/radix/primitives/accordion.pyi index dc2f6b853..20bc8dfe0 100644 --- a/reflex/components/radix/primitives/accordion.pyi +++ b/reflex/components/radix/primitives/accordion.pyi @@ -9,41 +9,95 @@ from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style from typing import Any, Dict, List, Literal, Optional, Union from reflex.components.component import Component, ComponentNamespace -from reflex.components.core.match import Match +from reflex.components.core.colors import color from reflex.components.lucide.icon import Icon from reflex.components.radix.primitives.base import RadixPrimitiveComponent from reflex.components.radix.themes.base import LiteralAccentColor -from reflex.style import ( - Style, - convert_dict_to_style_and_format_emotion, - format_as_emotion, -) +from reflex.style import Style from reflex.utils import imports -from reflex.vars import BaseVar, Var, VarData, get_uuid_string_var +from reflex.vars import Var, get_uuid_string_var LiteralAccordionType = Literal["single", "multiple"] LiteralAccordionDir = Literal["ltr", "rtl"] LiteralAccordionOrientation = Literal["vertical", "horizontal"] -LiteralAccordionRootVariant = Literal["classic", "soft", "surface", "outline", "ghost"] -LiteralAccordionRootColorScheme = Literal["primary", "accent"] +LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"] 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): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore cls, *children, + color_scheme: Optional[ + Union[ + Var[ + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ] + ], + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ], + ] + ] = None, + variant: Optional[ + Union[ + Var[Literal["classic", "soft", "surface", "outline", "ghost"]], + Literal["classic", "soft", "surface", "outline", "ghost"], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -102,6 +156,8 @@ class AccordionComponent(RadixPrimitiveComponent): Args: *children: The children of the component. + color_scheme: The color scheme of the component. + variant: The variant 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. @@ -208,8 +264,6 @@ class AccordionRoot(AccordionComponent): ], ] ] = None, - _dynamic_themes: Optional[Union[Var[dict], dict]] = None, - _var_data: Optional[VarData] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -278,10 +332,8 @@ 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. - _var_data: The var_data associated with the component. + variant: The variant of the component. + color_scheme: The color scheme 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. @@ -296,6 +348,7 @@ class AccordionRoot(AccordionComponent): """ ... def get_event_triggers(self) -> Dict[str, Any]: ... + def add_style(self): ... class AccordionItem(AccordionComponent): @overload @@ -307,6 +360,74 @@ class AccordionItem(AccordionComponent): content: Optional[Union[Component, Var]] = None, value: Optional[Union[Var[str], str]] = None, disabled: Optional[Union[Var[bool], bool]] = None, + color_scheme: Optional[ + Union[ + Var[ + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ] + ], + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ], + ] + ] = None, + variant: Optional[ + Union[ + Var[Literal["classic", "soft", "surface", "outline", "ghost"]], + Literal["classic", "soft", "surface", "outline", "ghost"], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -364,11 +485,13 @@ class AccordionItem(AccordionComponent): """Create an accordion item. Args: + *children: The list of children to use if header and content are not provided. header: The header of the accordion item. content: The content of the accordion item. - *children: The list of children to use if header and content are not provided. value: A unique identifier for the item. disabled: When true, prevents the user from interacting with the item. + color_scheme: The color scheme of the component. + variant: The variant 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. @@ -382,6 +505,7 @@ class AccordionItem(AccordionComponent): The accordion item. """ ... + def add_style(self) -> Style | None: ... class AccordionHeader(AccordionComponent): @overload @@ -389,6 +513,74 @@ class AccordionHeader(AccordionComponent): def create( # type: ignore cls, *children, + color_scheme: Optional[ + Union[ + Var[ + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ] + ], + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ], + ] + ] = None, + variant: Optional[ + Union[ + Var[Literal["classic", "soft", "surface", "outline", "ghost"]], + Literal["classic", "soft", "surface", "outline", "ghost"], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -447,6 +639,8 @@ class AccordionHeader(AccordionComponent): Args: *children: The children of the component. + color_scheme: The color scheme of the component. + variant: The variant 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. @@ -460,6 +654,9 @@ class AccordionHeader(AccordionComponent): The Accordion header Component. """ ... + def add_style(self) -> Style | None: ... + +cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)" class AccordionTrigger(AccordionComponent): @overload @@ -467,6 +664,74 @@ class AccordionTrigger(AccordionComponent): def create( # type: ignore cls, *children, + color_scheme: Optional[ + Union[ + Var[ + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ] + ], + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ], + ] + ] = None, + variant: Optional[ + Union[ + Var[Literal["classic", "soft", "surface", "outline", "ghost"]], + Literal["classic", "soft", "surface", "outline", "ghost"], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -525,6 +790,8 @@ class AccordionTrigger(AccordionComponent): Args: *children: The children of the component. + color_scheme: The color scheme of the component. + variant: The variant 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. @@ -538,6 +805,7 @@ class AccordionTrigger(AccordionComponent): The Accordion trigger Component. """ ... + def add_style(self) -> Style | None: ... class AccordionIcon(Icon): @overload @@ -618,11 +886,80 @@ class AccordionIcon(Icon): ... class AccordionContent(AccordionComponent): + def add_imports(self) -> imports.ImportDict: ... @overload @classmethod def create( # type: ignore cls, *children, + color_scheme: Optional[ + Union[ + Var[ + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ] + ], + Literal[ + "tomato", + "red", + "ruby", + "crimson", + "pink", + "plum", + "purple", + "violet", + "iris", + "indigo", + "blue", + "cyan", + "teal", + "jade", + "green", + "grass", + "brown", + "orange", + "sky", + "mint", + "lime", + "yellow", + "amber", + "gold", + "bronze", + "gray", + ], + ] + ] = None, + variant: Optional[ + Union[ + Var[Literal["classic", "soft", "surface", "outline", "ghost"]], + Literal["classic", "soft", "surface", "outline", "ghost"], + ] + ] = None, as_child: Optional[Union[Var[bool], bool]] = None, style: Optional[Style] = None, key: Optional[Any] = None, @@ -681,6 +1018,8 @@ class AccordionContent(AccordionComponent): Args: *children: The children of the component. + color_scheme: The color scheme of the component. + variant: The variant 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. @@ -694,6 +1033,8 @@ class AccordionContent(AccordionComponent): The Accordion content Component. """ ... + def add_custom_code(self) -> list[str]: ... + def add_style(self) -> Style | None: ... class Accordion(ComponentNamespace): content = staticmethod(AccordionContent.create) diff --git a/reflex/components/radix/primitives/form.py b/reflex/components/radix/primitives/form.py index 03975a2d4..3e9e38f38 100644 --- a/reflex/components/radix/primitives/form.py +++ b/reflex/components/radix/primitives/form.py @@ -4,10 +4,11 @@ from __future__ import annotations from typing import Any, Dict, Literal -from reflex.components.component import Component, ComponentNamespace +from reflex.components.component import ComponentNamespace from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot from reflex.constants.event import EventTriggers +from reflex.style import Style from reflex.vars import Var from .base import RadixPrimitiveComponentWithClassName @@ -37,11 +38,13 @@ class FormRoot(FormComponent, HTMLForm): EventTriggers.ON_CLEAR_SERVER_ERRORS: lambda: [], } - def _apply_theme(self, theme: Component): - return { - "width": "260px", - **self.style, - } + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style({"width": "260px"}) class FormField(FormComponent): @@ -57,12 +60,13 @@ class FormField(FormComponent): # Flag to mark the form field as invalid, for server side validation. server_invalid: Var[bool] - def _apply_theme(self, theme: Component): - return { - "display": "grid", - "margin_bottom": "10px", - **self.style, - } + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style({"display": "grid", "margin_bottom": "10px"}) class FormLabel(FormComponent): @@ -72,13 +76,13 @@ class FormLabel(FormComponent): alias = "RadixFormLabel" - def _apply_theme(self, theme: Component): - return { - "font_size": "15px", - "font_weight": "500", - "line_height": "35px", - **self.style, - } + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style({"font_size": "15px", "font_weight": "500", "line_height": "35px"}) class FormControl(FormComponent): @@ -145,13 +149,13 @@ class FormMessage(FormComponent): # Forces the message to be shown. This is useful when using server-side validation. force_match: Var[bool] - def _apply_theme(self, theme: Component): - return { - "font_size": "13px", - "opacity": "0.8", - "color": "white", - **self.style, - } + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style({"font_size": "13px", "opacity": "0.8", "color": "white"}) class FormValidityState(FormComponent): diff --git a/reflex/components/radix/primitives/form.pyi b/reflex/components/radix/primitives/form.pyi index 80a2cdff3..8e1631825 100644 --- a/reflex/components/radix/primitives/form.pyi +++ b/reflex/components/radix/primitives/form.pyi @@ -8,10 +8,11 @@ from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style from typing import Any, Dict, Literal -from reflex.components.component import Component, ComponentNamespace +from reflex.components.component import ComponentNamespace from reflex.components.el.elements.forms import Form as HTMLForm from reflex.components.radix.themes.components.text_field import TextFieldRoot from reflex.constants.event import EventTriggers +from reflex.style import Style from reflex.vars import Var from .base import RadixPrimitiveComponentWithClassName @@ -95,6 +96,7 @@ class FormComponent(RadixPrimitiveComponentWithClassName): class FormRoot(FormComponent, HTMLForm): def get_event_triggers(self) -> Dict[str, Any]: ... + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -273,6 +275,7 @@ class FormRoot(FormComponent, HTMLForm): ... class FormField(FormComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -355,6 +358,7 @@ class FormField(FormComponent): ... class FormLabel(FormComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -528,6 +532,7 @@ LiteralMatcher = Literal[ ] class FormMessage(FormComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/primitives/progress.py b/reflex/components/radix/primitives/progress.py index 08fa14b05..313d6aeb6 100644 --- a/reflex/components/radix/primitives/progress.py +++ b/reflex/components/radix/primitives/progress.py @@ -28,20 +28,24 @@ class ProgressRoot(ProgressComponent): # Override theme radius for progress bar: "none" | "small" | "medium" | "large" | "full" radius: Var[LiteralRadius] - def _apply_theme(self, theme: Component): + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ if self.radius is not None: self.custom_attrs["data-radius"] = self.radius - self.style = Style( + return Style( { "position": "relative", "overflow": "hidden", - "background": "var(--gray-a3)", + "background": color("gray", 3, alpha=True), "border_radius": "max(var(--radius-2), var(--radius-full))", "width": "100%", "height": "20px", - "boxShadow": "inset 0 0 0 1px var(--gray-a5)", - **self.style, + "boxShadow": f"inset 0 0 0 1px {color('gray', 5, alpha=True)}", } ) @@ -65,22 +69,26 @@ class ProgressIndicator(ProgressComponent): # The color scheme of the progress indicator. color_scheme: Var[LiteralAccentColor] - def _apply_theme(self, theme: Component): + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ if self.color_scheme is not None: self.custom_attrs["data-accent-color"] = self.color_scheme - self.style = Style( + return Style( { "background_color": color("accent", 9), "width": "100%", "height": "100%", - f"transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", + "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", "&[data_state='loading']": { "transition": f"transform {DEFAULT_ANIMATION_DURATION}ms linear", }, "transform": f"translateX(calc(-100% + ({self.value} / {self.max} * 100%)))", # type: ignore "boxShadow": "inset 0 0 0 1px var(--gray-a5)", - **self.style, } ) diff --git a/reflex/components/radix/primitives/progress.pyi b/reflex/components/radix/primitives/progress.pyi index 7ef8ae6f0..eb0149496 100644 --- a/reflex/components/radix/primitives/progress.pyi +++ b/reflex/components/radix/primitives/progress.pyi @@ -95,6 +95,7 @@ class ProgressComponent(RadixPrimitiveComponentWithClassName): ... class ProgressRoot(ProgressComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -180,6 +181,7 @@ class ProgressRoot(ProgressComponent): ... class ProgressIndicator(ProgressComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/primitives/slider.py b/reflex/components/radix/primitives/slider.py index 94560c3f0..2e0b1ef49 100644 --- a/reflex/components/radix/primitives/slider.py +++ b/reflex/components/radix/primitives/slider.py @@ -59,8 +59,13 @@ class SliderRoot(SliderComponent): "on_value_commit": lambda e0: [e0], # trigger when thumb is released } - def _apply_theme(self, theme: Component): - self.style = Style( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "position": "relative", "display": "flex", @@ -74,7 +79,6 @@ class SliderRoot(SliderComponent): "width": "20px", "height": "100px", }, - **self.style, } ) @@ -85,18 +89,20 @@ class SliderTrack(SliderComponent): tag = "Track" alias = "RadixSliderTrack" - def _apply_theme(self, theme: Component): - self.style = Style( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "position": "relative", "flex_grow": "1", "background_color": "black", "border_radius": "9999px", "height": "3px", - "&[data-orientation='vertical']": { - "width": "3px", - }, - **self.style, + "&[data-orientation='vertical']": {"width": "3px"}, } ) @@ -107,16 +113,18 @@ class SliderRange(SliderComponent): tag = "Range" alias = "RadixSliderRange" - def _apply_theme(self, theme: Component): - self.style = Style( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "position": "absolute", "background_color": "white", "height": "100%", - "&[data-orientation='vertical']": { - "width": "100%", - }, - **self.style, + "&[data-orientation='vertical']": {"width": "100%"}, } ) @@ -127,8 +135,13 @@ class SliderThumb(SliderComponent): tag = "Thumb" alias = "RadixSliderThumb" - def _apply_theme(self, theme: Component): - self.style = Style( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "display": "block", "width": "20px", @@ -143,7 +156,6 @@ class SliderThumb(SliderComponent): "outline": "none", "box_shadow": "0 0 0 4px gray", }, - **self.style, } ) diff --git a/reflex/components/radix/primitives/slider.pyi b/reflex/components/radix/primitives/slider.pyi index 7b8384eff..1f837d732 100644 --- a/reflex/components/radix/primitives/slider.pyi +++ b/reflex/components/radix/primitives/slider.pyi @@ -96,6 +96,7 @@ class SliderComponent(RadixPrimitiveComponentWithClassName): class SliderRoot(SliderComponent): def get_event_triggers(self) -> Dict[str, Any]: ... + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -196,6 +197,7 @@ class SliderRoot(SliderComponent): ... class SliderTrack(SliderComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -274,6 +276,7 @@ class SliderTrack(SliderComponent): ... class SliderRange(SliderComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore @@ -352,6 +355,7 @@ class SliderRange(SliderComponent): ... class SliderThumb(SliderComponent): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/themes/components/icon_button.py b/reflex/components/radix/themes/components/icon_button.py index 684ffc1e6..cd21501db 100644 --- a/reflex/components/radix/themes/components/icon_button.py +++ b/reflex/components/radix/themes/components/icon_button.py @@ -1,4 +1,5 @@ """Interactive components provided by @radix-ui/themes.""" +from __future__ import annotations from typing import Literal @@ -6,6 +7,7 @@ from reflex import el from reflex.components.component import Component from reflex.components.core.match import Match from reflex.components.lucide import Icon +from reflex.style import Style from reflex.vars import Var from ..base import ( @@ -68,25 +70,25 @@ class IconButton(el.Button, RadixLoadingProp, RadixThemesComponent): "IconButton requires a child icon. Pass a string as the first child or a rx.icon." ) if "size" in props: + RADIX_TO_LUCIDE_SIZE = {"1": "12px", "2": "24px", "3": "36px", "4": "48px"} + if isinstance(props["size"], str): - RADIX_TO_LUCIDE_SIZE = { - "1": "12px", - "2": "24px", - "3": "36px", - "4": "48px", - } children[0].size = RADIX_TO_LUCIDE_SIZE[props["size"]] else: children[0].size = Match.create( props["size"], - ("1", "12px"), - ("2", "24px"), - ("3", "36px"), - ("4", "48px"), + *[(size, px) for size, px in RADIX_TO_LUCIDE_SIZE.items()], "12px", ) - props.setdefault("padding", "6px") return super().create(*children, **props) + def add_style(self): + """Add style to the component. + + Returns: + The style of the component. + """ + return Style({"padding": "6px"}) + icon_button = IconButton.create diff --git a/reflex/components/radix/themes/components/icon_button.pyi b/reflex/components/radix/themes/components/icon_button.pyi index a883f6595..3e3657dbf 100644 --- a/reflex/components/radix/themes/components/icon_button.pyi +++ b/reflex/components/radix/themes/components/icon_button.pyi @@ -12,6 +12,7 @@ from reflex import el from reflex.components.component import Component from reflex.components.core.match import Match from reflex.components.lucide import Icon +from reflex.style import Style from reflex.vars import Var from ..base import ( LiteralAccentColor, @@ -280,5 +281,6 @@ class IconButton(el.Button, RadixLoadingProp, RadixThemesComponent): The IconButton component. """ ... + def add_style(self): ... icon_button = IconButton.create diff --git a/reflex/components/radix/themes/layout/center.py b/reflex/components/radix/themes/layout/center.py index b6a8faa8e..16441876a 100644 --- a/reflex/components/radix/themes/layout/center.py +++ b/reflex/components/radix/themes/layout/center.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.components.component import Component +from reflex.style import Style from .flex import Flex @@ -10,8 +10,13 @@ from .flex import Flex class Center(Flex): """A center component.""" - def _apply_theme(self, theme: Component): - self.style.update( + def add_style(self) -> Style | None: + """Add style that center the content. + + Returns: + The style of the component. + """ + return Style( { "display": "flex", "align_items": "center", diff --git a/reflex/components/radix/themes/layout/center.pyi b/reflex/components/radix/themes/layout/center.pyi index ef764592a..0fd45f940 100644 --- a/reflex/components/radix/themes/layout/center.pyi +++ b/reflex/components/radix/themes/layout/center.pyi @@ -7,10 +7,11 @@ 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 reflex.components.component import Component +from reflex.style import Style from .flex import Flex class Center(Flex): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/components/radix/themes/layout/list.py b/reflex/components/radix/themes/layout/list.py index 2bc80b6a4..87b9cf012 100644 --- a/reflex/components/radix/themes/layout/list.py +++ b/reflex/components/radix/themes/layout/list.py @@ -1,4 +1,5 @@ """List components.""" +from __future__ import annotations from typing import Iterable, Literal, Optional, Union @@ -77,12 +78,16 @@ class BaseList(Component): style["gap"] = props["gap"] return super().create(*children, **props) - def _apply_theme(self, theme: Component): - self.style = Style( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "direction": "column", "list_style_position": "inside", - **self.style, } ) diff --git a/reflex/components/radix/themes/layout/list.pyi b/reflex/components/radix/themes/layout/list.pyi index bd9c1c633..a736f106c 100644 --- a/reflex/components/radix/themes/layout/list.pyi +++ b/reflex/components/radix/themes/layout/list.pyi @@ -157,6 +157,7 @@ class BaseList(Component): """ ... + def add_style(self) -> Style | None: ... class UnorderedList(BaseList, Ul): @overload @@ -165,7 +166,7 @@ class UnorderedList(BaseList, Ul): cls, *children, items: Optional[Union[Var[Iterable], Iterable]] = None, - list_style_type: Optional[Literal["none", "disc", "circle", "square"]] = "disc", + list_style_type: Optional[LiteralListStyleTypeUnordered] = "disc", access_key: Optional[ Union[Var[Union[str, int, bool]], Union[str, int, bool]] ] = None, @@ -302,24 +303,7 @@ class OrderedList(BaseList, Ol): cls, *children, items: Optional[Union[Var[Iterable], Iterable]] = None, - list_style_type: Optional[ - Literal[ - "none", - "decimal", - "decimal-leading-zero", - "lower-roman", - "upper-roman", - "lower-greek", - "lower-latin", - "upper-latin", - "armenian", - "georgian", - "lower-alpha", - "upper-alpha", - "hiragana", - "katakana", - ] - ] = "decimal", + list_style_type: Optional[LiteralListStyleTypeOrdered] = "decimal", reversed: Optional[ Union[Var[Union[str, int, bool]], Union[str, int, bool]] ] = None, diff --git a/reflex/components/radix/themes/layout/spacer.py b/reflex/components/radix/themes/layout/spacer.py index 33a790216..6d7ab9aaf 100644 --- a/reflex/components/radix/themes/layout/spacer.py +++ b/reflex/components/radix/themes/layout/spacer.py @@ -2,7 +2,7 @@ from __future__ import annotations -from reflex.components.component import Component +from reflex.style import Style from .flex import Flex @@ -10,8 +10,13 @@ from .flex import Flex class Spacer(Flex): """A spacer component.""" - def _apply_theme(self, theme: Component): - self.style.update( + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ + return Style( { "flex": 1, "justify_self": "stretch", diff --git a/reflex/components/radix/themes/layout/spacer.pyi b/reflex/components/radix/themes/layout/spacer.pyi index 5f9d654a3..733501489 100644 --- a/reflex/components/radix/themes/layout/spacer.pyi +++ b/reflex/components/radix/themes/layout/spacer.pyi @@ -7,10 +7,11 @@ 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 reflex.components.component import Component +from reflex.style import Style from .flex import Flex class Spacer(Flex): + def add_style(self) -> Style | None: ... @overload @classmethod def create( # type: ignore diff --git a/reflex/constants/colors.py b/reflex/constants/colors.py index 5408e15b1..ddd093f25 100644 --- a/reflex/constants/colors.py +++ b/reflex/constants/colors.py @@ -37,6 +37,8 @@ ColorType = Literal[ "bronze", "gray", "accent", + "black", + "white", ] ShadeType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] diff --git a/reflex/experimental/layout.py b/reflex/experimental/layout.py index 28729c362..5041b9356 100644 --- a/reflex/experimental/layout.py +++ b/reflex/experimental/layout.py @@ -1,5 +1,7 @@ """To experiment with layout component, move them to reflex/components later.""" +from __future__ import annotations + from reflex import color, cond from reflex.components.base.fragment import Fragment from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf @@ -9,6 +11,7 @@ from reflex.components.radix.themes.layout import Box, Container, HStack from reflex.event import call_script from reflex.experimental import hooks from reflex.state import ComponentState +from reflex.style import Style from reflex.vars import Var @@ -26,23 +29,38 @@ class Sidebar(Box, MemoizationLeaf): Returns: The sidebar component. """ - props.setdefault("border_right", f"1px solid {color('accent', 12)}") - props.setdefault("background_color", color("accent", 1)) - props.setdefault("width", "20vw") - props.setdefault("height", "100vh") - props.setdefault("position", "fixed") + # props.setdefault("border_right", f"1px solid {color('accent', 12)}") + # props.setdefault("background_color", color("accent", 1)) + # props.setdefault("width", "20vw") + # props.setdefault("height", "100vh") + # props.setdefault("position", "fixed") return super().create( Box.create(*children, **props), # sidebar for content Box.create(width=props.get("width")), # spacer for layout ) - def _apply_theme(self, theme: Component | None): + def add_style(self) -> Style | None: + """Add style to the component. + + Returns: + The style of the component. + """ sidebar: Component = self.children[-2] # type: ignore spacer: Component = self.children[-1] # type: ignore open = self.State.open if self.State else Var.create("open") # type: ignore sidebar.style["display"] = spacer.style["display"] = cond(open, "block", "none") + return Style( + { + "position": "fixed", + "border_right": f"1px solid {color('accent', 12)}", + "background_color": color("accent", 1), + "width": "20vw", + "height": "100vh", + } + ) + def _get_hooks(self) -> Var | None: return hooks.useState("open", "true") if not self.State else None diff --git a/reflex/style.py b/reflex/style.py index 179e4ff19..d77c2bb7c 100644 --- a/reflex/style.py +++ b/reflex/style.py @@ -159,12 +159,17 @@ def format_style_key(key: str) -> Tuple[str, ...]: class Style(dict): """A style dictionary.""" - def __init__(self, style_dict: dict | None = None): + def __init__(self, style_dict: dict | None = None, **kwargs): """Initialize the style. Args: style_dict: The style dictionary. + kwargs: Other key value pairs to apply to the dict update. """ + if style_dict: + style_dict.update(kwargs) + else: + style_dict = kwargs style_dict, self._var_data = convert(style_dict or {}) super().__init__(style_dict) diff --git a/tests/components/core/test_foreach.py b/tests/components/core/test_foreach.py index db0488e68..34e43e94d 100644 --- a/tests/components/core/test_foreach.py +++ b/tests/components/core/test_foreach.py @@ -2,7 +2,7 @@ from typing import Dict, List, Set, Tuple, Union import pytest -from reflex.components import box, foreach, text, theme +from reflex.components import box, foreach, text from reflex.components.core import Foreach from reflex.state import BaseState from reflex.vars import Var @@ -220,15 +220,6 @@ def test_foreach_render(state_var, render_fn, render_dict): seen_index_vars.add(arg_index._var_name) -def test_foreach_apply_theme(): - """Test that the foreach component applies the theme.""" - tag = Foreach.create(ForEachState.colors_list, display_color) # type: ignore - _theme = theme() - tag.apply_theme(_theme) - assert tag.theme == _theme - tag.render() - - def test_foreach_bad_annotations(): """Test that the foreach component raises a TypeError if the iterable is of type Any.""" with pytest.raises(TypeError): diff --git a/tests/components/lucide/test_icon.py b/tests/components/lucide/test_icon.py index e5f71ad22..d61f3a9aa 100644 --- a/tests/components/lucide/test_icon.py +++ b/tests/components/lucide/test_icon.py @@ -1,7 +1,6 @@ import pytest from reflex.components.lucide.icon import LUCIDE_ICON_LIST, RENAMED_ICONS_05, Icon -from reflex.components.radix.themes.base import Theme from reflex.utils import format @@ -17,7 +16,7 @@ RENAMED_TAGS = [tag for tag in RENAMED_ICONS_05.items()] @pytest.mark.parametrize("tag, new_tag", RENAMED_TAGS) def test_icon_renamed_tags(tag, new_tag): Icon.create(tag) - # need a PR so we can pass the following test. Currently it fails and uses the old tag as the import. + # TODO: need a PR so we can pass the following test. Currently it fails and uses the old tag as the import. # assert icon.alias == f"Lucide{format.to_title_case(new_tag)}Icon" @@ -36,6 +35,6 @@ def test_icon_multiple_children(): _ = Icon.create("activity", "child1", "child2") -def test_icon_apply_theme(): +def test_icon_add_style(): ic = Icon.create("activity") - ic._apply_theme(Theme()) + assert ic.add_style() is None diff --git a/tests/components/radix/test_icon_button.py b/tests/components/radix/test_icon_button.py index 5791896fc..852c0c97c 100644 --- a/tests/components/radix/test_icon_button.py +++ b/tests/components/radix/test_icon_button.py @@ -1,19 +1,21 @@ import pytest from reflex.components.lucide.icon import Icon -from reflex.components.radix.themes.base import Theme from reflex.components.radix.themes.components.icon_button import IconButton +from reflex.style import Style from reflex.vars import Var def test_icon_button(): ib1 = IconButton.create("activity") - ib1._apply_theme(Theme.create()) assert isinstance(ib1, IconButton) ib2 = IconButton.create(Icon.create("activity")) assert isinstance(ib2, IconButton) + assert isinstance(ib1.add_style(), Style) + assert isinstance(ib2.add_style(), Style) + def test_icon_button_no_child(): with pytest.raises(ValueError):