Compare commits

...

12 Commits

Author SHA1 Message Date
Masen Furer
25c1406e7f
bump to 0.5.0.post1 2024-05-14 18:49:03 -07:00
Elijah Ahianyo
7d4609ba60
[REF-2814]Throw Warning for Projects Created in OneDrive on Windows (#3304)
* Throw Warning for Projects Created in OneDrive on Windows

* precommit

* remove dead code

* REFLEX_USE_NPM escape hatch to opt out of bun

In some unsupported environments, we need to just not use bun. Further
investigation needed.

---------

Co-authored-by: Masen Furer <m_github@0x26.net>
2024-05-14 18:48:41 -07:00
Masen Furer
52e0507c40
Restore the rx.color_mode.switch (#3294)
This was tentatively removed for 0.5.0 without a deprecation period, so adding
it back to avoid immediate breakage in existing apps.
2024-05-13 16:05:41 -07:00
Eric Brown
5e1fe07e0b
Typo in contact email address (#3292)
The demo template includes a drop down menu item named Contact
has an email address to the founders. However, the email address
includes an extraneous = character in the domain part making it
invalid.

Signed-off-by: Eric Brown <eric_wade_brown@yahoo.com>
2024-05-13 16:05:41 -07:00
Masen Furer
d520b34762
Only import PyiGenerator when needed (#3291)
Speed up reflex invocation time for `--help` by 1s
2024-05-13 16:05:41 -07:00
Masen Furer
7727c14758
accordion: unify border radius specification (#3290) 2024-05-13 16:05:40 -07:00
Masen Furer
7e6fd44732
pyproject: bump version to 0.5.0 (#3287) 2024-05-13 16:05:40 -07:00
Masen Furer
69d5c02cc5
Allow Component.add_style to return a regular dict (#3264)
* Allow `Component.add_style` to return a regular dict

It's more convenient to allow returning a regular dict without having to import
and wrap the value in `rx.style.Style`.

If the dict contains any Var or encoded VarData f-strings, these will be picked
up when the plain dicts are passed to Style.update().

Because Style.update already merges VarData, there is no reason to explicitly
merge it again in this function; this change keeps the merging logic inside the
Style class.

* Test for Style.update with existing Style with _var_data and kwargs

Should retain the _var_data from the original Style instance

* style: Avoid losing VarData in Style.update

If a Style class with _var_data is passed to `Style.update` along with kwargs,
then the _var_data was lost in the double-splat dictionary expansion.

Instead, only apply the kwargs to an existing or new Style instance to retain
_var_data and properly convert values.

* add_style return annotation is Dict[str, Any]

* nit: use lowercase dict in annotation
2024-05-10 20:27:40 -07:00
Masen Furer
09dfd031d7
rx.accordion customizability overhaul (#3266)
* rx.accordion customizability overhaul

* fix low contrast font in "classic" variant
* relative font sizing, so setting font_size on the root works now
* use CSS vars wherever possible to make downstream styling easier
* optional dividers between accordion items
* support `radius`
* support `duration` and `easing` for controlling the animation
* fix animation jank while keeping padding_y
* lookup `data-variant` via CSS instead of passing props in python
* fix "surface" variant to use `--accent-surface` as radix intended

* Restore default variant: "classic"

* Fix accordion primitive radius
2024-05-10 20:27:39 -07:00
seewind
4e58716378
#3185 fix tailwind.config.js: support corePlugins, important, prefix, separator (#3260) 2024-05-10 20:27:39 -07:00
Masen Furer
4f7ccb57bc
[REF-2802] Foreach should respect modifications to children (#3263)
* Unit tests for add_style and component styles with foreach

The styles should be correctly applied for components that are rendered as part
of a foreach.

* [REF-2802] Foreach should respect modifications to children

Components are mutable, and there is logic that depends on walking through the
component tree and making modifications to components along the way. These
modifications _must_ be respected by foreach for consistency.

Modifications necessary to fix the bug:

* Change the hash function in `_render` to get a hash over the render_fn's
  `__code__` object. This way we get a stable hash without having to call the
  render function with bogus values.
* Call the render function once during `create` and save the result as a child
  of the Foreach component (tree walks will modify this instance).
* Directly render the original (and possibly modified) child component instead
  of calling the render_fn again and creating a new component instance at
  render time.

Additional changes because they're nice:

* Deprecate passing `**props` to `rx.foreach`. No one should have been
  doing this anyway, because it just does not work in any reasonable way.
* Raise `ForeachVarError` when the iterable type is Any
* Raise `ForeachRenderError` when the render function does not take 1 or 2 args.
* Link to the foreach component docs when either of those errors are hit.
* Change the `iterable` arg in `create` to accept `Var[Iterable] | Iterable`
  for better typing support (and remove some type: ignore comments)
* Simplify `_render` and `render` methods -- remove unused and potentially
  confusing code.

* Fixup: `to_bytes` requires `byteorder` arg before py3.11
2024-05-10 20:27:39 -07:00
Masen Furer
601391fca9
form: default width="100%" (#3261) 2024-05-10 20:27:39 -07:00
16 changed files with 608 additions and 178 deletions

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "reflex"
version = "0.4.9"
version = "0.5.0.post1"
description = "Web apps in pure Python."
license = "Apache-2.0"
authors = [

View File

@ -44,7 +44,7 @@ def template(main_content: Callable[[], rx.Component]) -> rx.Component:
),
rx.chakra.menu_item(
rx.chakra.link(
"Contact", href="mailto:founders@=reflex.dev", width="100%"
"Contact", href="mailto:founders@reflex.dev", width="100%"
)
),
),

View File

@ -17,4 +17,16 @@ module.exports = {
{% if darkMode is defined %}
darkMode: {{darkMode|json_dumps}},
{% endif %}
{% if corePlugins is defined %}
corePlugins: {{corePlugins|json_dumps}},
{% endif %}
{% if important is defined %}
important: {{important|json_dumps}},
{% endif %}
{% if prefix is defined %}
prefix: {{prefix|json_dumps}},
{% endif %}
{% if separator is defined %}
separator: {{separator|json_dumps}},
{% endif %}
};

View File

@ -781,7 +781,7 @@ class Component(BaseComponent, ABC):
return cls(children=children, **props)
def add_style(self) -> Style | None:
def add_style(self) -> dict[str, Any] | None:
"""Add style to the component.
Downstream components can override this method to return a style dict
@ -801,20 +801,16 @@ class Component(BaseComponent, ABC):
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:

View File

@ -2,16 +2,24 @@
from __future__ import annotations
import inspect
from hashlib import md5
from typing import Any, Callable, Iterable
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.tags import IterTag
from reflex.constants import MemoizationMode
from reflex.utils import console
from reflex.vars import Var
class ForeachVarError(TypeError):
"""Raised when the iterable type is Any."""
class ForeachRenderError(TypeError):
"""Raised when there is an error with the foreach render function."""
class Foreach(Component):
"""A component that takes in an iterable and a render function and renders a list of components."""
@ -24,56 +32,84 @@ class Foreach(Component):
render_fn: Callable = Fragment.create
@classmethod
def create(cls, iterable: Var[Iterable], render_fn: Callable, **props) -> Foreach:
def create(
cls,
iterable: Var[Iterable] | Iterable,
render_fn: Callable,
**props,
) -> Foreach:
"""Create a foreach component.
Args:
iterable: The iterable to create components from.
render_fn: A function from the render args to the component.
**props: The attributes to pass to each child component.
**props: The attributes to pass to each child component (deprecated).
Returns:
The foreach component.
Raises:
TypeError: If the iterable is of type Any.
ForeachVarError: If the iterable is of type Any.
"""
iterable = Var.create(iterable) # type: ignore
if props:
console.deprecate(
feature_name="Passing props to rx.foreach",
reason="it does not have the intended effect and may be confusing",
deprecation_version="0.5.0",
removal_version="0.6.0",
)
iterable = Var.create_safe(iterable)
if iterable._var_type == Any:
raise TypeError(
f"Could not foreach over var of type Any. (If you are trying to foreach over a state var, add a type annotation to the var.)"
raise ForeachVarError(
f"Could not foreach over var `{iterable._var_full_name}` of type Any. "
"(If you are trying to foreach over a state var, add a type annotation to the var). "
"See https://reflex.dev/docs/library/layout/foreach/"
)
component = cls(
iterable=iterable,
render_fn=render_fn,
**props,
)
# Keep a ref to a rendered component to determine correct imports.
component.children = [
component._render(props=dict(index_var_name="i")).render_component()
]
# Keep a ref to a rendered component to determine correct imports/hooks/styles.
component.children = [component._render().render_component()]
return component
def _render(self, props: dict[str, Any] | None = None) -> IterTag:
props = {} if props is None else props.copy()
def _render(self) -> IterTag:
props = {}
# Determine the arg var name based on the params accepted by render_fn.
render_sig = inspect.signature(self.render_fn)
params = list(render_sig.parameters.values())
if len(params) >= 1:
props.setdefault("arg_var_name", params[0].name)
if len(params) >= 2:
# Validate the render function signature.
if len(params) == 0 or len(params) > 2:
raise ForeachRenderError(
"Expected 1 or 2 parameters in foreach render function, got "
f"{[p.name for p in params]}. See https://reflex.dev/docs/library/layout/foreach/"
)
if len(params) >= 1:
# Determine the arg var name based on the params accepted by render_fn.
props["arg_var_name"] = params[0].name
if len(params) == 2:
# Determine the index var name based on the params accepted by render_fn.
props.setdefault("index_var_name", params[1].name)
elif "index_var_name" not in props:
# Otherwise, use a deterministic index, based on the rendered code.
code_hash = md5(str(self.children[0].render()).encode("utf-8")).hexdigest()
props.setdefault("index_var_name", f"index_{code_hash}")
props["index_var_name"] = params[1].name
else:
# Otherwise, use a deterministic index, based on the render function bytecode.
code_hash = (
hash(self.render_fn.__code__)
.to_bytes(
length=8,
byteorder="big",
signed=True,
)
.hex()
)
props["index_var_name"] = f"index_{code_hash}"
return IterTag(
iterable=self.iterable,
render_fn=self.render_fn,
children=self.children,
**props,
)
@ -84,19 +120,9 @@ class Foreach(Component):
The dictionary for template of component.
"""
tag = self._render()
component = tag.render_component()
return dict(
tag.add_props(
**self.event_triggers,
key=self.key,
sx=self.style,
id=self.id,
class_name=self.class_name,
).set(
children=[component.render()],
props=tag.format_props(),
),
tag,
iterable_state=tag.iterable._var_full_name,
arg_name=tag.arg_var_name,
arg_index=tag.get_index_var_arg(),

View File

@ -6,9 +6,10 @@ from typing import Any, Dict, List, Literal, Optional, Union
from reflex.components.component import Component, ComponentNamespace
from reflex.components.core.colors import color
from reflex.components.core.cond import cond
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.components.radix.themes.base import LiteralAccentColor, LiteralRadius
from reflex.style import Style
from reflex.utils import imports
from reflex.vars import Var, get_uuid_string_var
@ -19,6 +20,32 @@ LiteralAccordionOrientation = Literal["vertical", "horizontal"]
LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
DEFAULT_ANIMATION_DURATION = 250
DEFAULT_ANIMATION_EASING = "cubic-bezier(0.87, 0, 0.13, 1)"
def _inherited_variant_selector(
variant: Var[LiteralAccordionVariant] | LiteralAccordionVariant,
*selectors: str,
) -> str:
"""Create a multi CSS selector for targeting variant against the given selectors.
Args:
variant: The variant to target.
selectors: The selectors to apply the variant to (default &)
Returns:
A CSS selector that is more specific on elements that directly set the variant.
"""
if not selectors:
selectors = ("&",)
# Prefer the `data-variant` that is set directly on the selector,
# but also inherit the `data-variant` from any parent element.
return ", ".join(
[
f"{selector}[data-variant='{variant}'], *:where([data-variant='{variant}']) {selector}"
for selector in selectors
]
)
class AccordionComponent(RadixPrimitiveComponent):
@ -30,14 +57,14 @@ class AccordionComponent(RadixPrimitiveComponent):
color_scheme: Var[LiteralAccentColor]
# The variant of the component.
variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
variant: Var[LiteralAccordionVariant]
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
if self.variant is not None:
self.custom_attrs["data-variant"] = self.variant
def _exclude_props(self) -> list[str]:
return ["color_scheme", "variant"]
@ -71,28 +98,27 @@ class AccordionRoot(AccordionComponent):
# The orientation of the accordion.
orientation: Var[LiteralAccordionOrientation]
# The variant of the accordion.
variant: Var[LiteralAccordionVariant] = Var.create_safe("classic")
# The radius of the accordion corners.
radius: Var[LiteralRadius]
# The time in milliseconds to animate open and close
duration: Var[int] = Var.create_safe(DEFAULT_ANIMATION_DURATION)
# The easing function to use for the animation.
easing: Var[str] = Var.create_safe(DEFAULT_ANIMATION_EASING)
# Whether to show divider lines between items.
show_dividers: Var[bool]
_valid_children: List[str] = ["AccordionItem"]
@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.
"""
for child in children:
if isinstance(child, AccordionItem):
child.color_scheme = props.get("color_scheme") # type: ignore
child.variant = props.get("variant") # type: ignore
return super().create(*children, **props)
def _exclude_props(self) -> list[str]:
return super()._exclude_props() + [
"radius",
"duration",
"easing",
"show_dividers",
]
def get_event_triggers(self) -> Dict[str, Any]:
"""Get the events triggers signatures for the component.
@ -111,30 +137,42 @@ class AccordionRoot(AccordionComponent):
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",
},
}
)
if self.radius is not None:
self.custom_attrs["data-radius"] = self.radius
if self.variant is None:
# The default variant is classic
self.custom_attrs["data-variant"] = "classic"
style = {
"border_radius": "var(--radius-4)",
"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": "var(--accent-surface)",
},
"&[data-variant='ghost']": {
"background_color": "none",
"box_shadow": "None",
},
"--animation-duration": f"{self.duration}ms",
"--animation-easing": self.easing,
}
if self.show_dividers is not None:
style["--divider-px"] = cond(self.show_dividers, "1px", "0")
else:
style["&[data-variant='outline']"]["--divider-px"] = "1px"
style["&[data-variant='surface']"]["--divider-px"] = "1px"
return Style(style)
class AccordionItem(AccordionComponent):
@ -185,23 +223,28 @@ class AccordionItem(AccordionComponent):
):
cls_name = f"{cls_name} AccordionItem"
color_scheme = props.get("color_scheme")
variant = props.get("variant")
if (header is not None) and (content is not None):
children = [
AccordionHeader.create(
AccordionTrigger.create(
header,
AccordionIcon.create(
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
color_scheme=props.get("color_scheme"),
variant=props.get("variant"),
color_scheme=color_scheme,
variant=variant,
),
AccordionContent.create(
content, color_scheme=props.get("color_scheme")
content,
color_scheme=color_scheme,
variant=variant,
),
]
@ -213,29 +256,35 @@ class AccordionItem(AccordionComponent):
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
divider_style = f"var(--divider-px) solid {color('gray', 6, alpha=True)}"
return Style(
{
"overflow": "hidden",
"width": "100%",
"margin_top": "1px",
"border_top": divider_style,
"&:first-child": {
"margin_top": 0,
"border_top_left_radius": "4px",
"border_top_right_radius": "4px",
"border_top": 0,
"border_top_left_radius": "var(--radius-4)",
"border_top_right_radius": "var(--radius-4)",
},
"&:last-child": {
"border_bottom_left_radius": "4px",
"border_bottom_right_radius": "4px",
"border_bottom_left_radius": "var(--radius-4)",
"border_bottom_right_radius": "var(--radius-4)",
},
"&:focus-within": {
"position": "relative",
"z_index": 1,
},
_inherited_variant_selector("ghost", "&:first-child"): {
"border_radius": 0,
"border_top": divider_style,
},
_inherited_variant_selector("ghost", "&:last-child"): {
"border_radius": 0,
"border_bottom": divider_style,
},
}
)
@ -271,17 +320,9 @@ class AccordionHeader(AccordionComponent):
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):
"""An accordion component."""
@ -313,24 +354,18 @@ class AccordionTrigger(AccordionComponent):
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),
"font_size": "1.1em",
"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",
"padding": "var(--space-3) var(--space-4)",
"width": "100%",
"box_shadow": f"0 var(--divider-px) 0 {color('gray', 6, alpha=True)}",
"&[data-state='open'] > .AccordionChevron": {
"transform": "rotate(180deg)",
},
@ -338,17 +373,15 @@ class AccordionTrigger(AccordionComponent):
"background_color": color("accent", 4),
},
"& > .AccordionChevron": {
"transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
"transition": f"transform var(--animation-duration) var(--animation-easing)",
},
"&[data-variant='classic']": {
"color": color("accent", 12),
"box_shadow": color("accent", 11),
_inherited_variant_selector("classic"): {
"color": "var(--accent-contrast)",
"&:hover": {
"background_color": color("accent", 10),
},
"& > .AccordionChevron": {
"color": color("accent", 12),
"transition": f"transform {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
"color": "var(--accent-contrast)",
},
},
}
@ -444,30 +477,31 @@ to {
The style of the component.
"""
slideDown = Var.create(
f"${{slideDown}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
f"${{slideDown}} var(--animation-duration) var(--animation-easing)",
_var_is_string=True,
)
slideUp = Var.create(
f"${{slideUp}} {DEFAULT_ANIMATION_DURATION}ms {cubic_bezier}",
f"${{slideUp}} var(--animation-duration) var(--animation-easing)",
_var_is_string=True,
)
return Style(
{
"overflow": "hidden",
"font_size": "10px",
"color": color("accent", 11),
"background_color": color("accent", 3),
"padding": "0 15px",
"padding_x": "var(--space-4)",
# Apply before and after content to avoid height animation jank.
"&:before, &:after": {
"content": "' '",
"display": "block",
"height": "var(--space-3)",
},
"&[data-state='open']": {"animation": slideDown},
"&[data-state='closed']": {"animation": slideUp},
"&[data-variant='classic']": {
"color": color("accent", 12),
"background_color": color("accent", 9),
_inherited_variant_selector("classic"): {
"color": "var(--accent-contrast)",
},
"&[data-variant='outline']": {"background_color": "transparent"},
"&[data-variant='ghost']": {"background_color": "transparent"},
}
)

View File

@ -10,9 +10,10 @@ 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.colors import color
from reflex.components.core.cond import cond
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.components.radix.themes.base import LiteralAccentColor, LiteralRadius
from reflex.style import Style
from reflex.utils import imports
from reflex.vars import Var, get_uuid_string_var
@ -22,6 +23,7 @@ LiteralAccordionDir = Literal["ltr", "rtl"]
LiteralAccordionOrientation = Literal["vertical", "horizontal"]
LiteralAccordionVariant = Literal["classic", "soft", "surface", "outline", "ghost"]
DEFAULT_ANIMATION_DURATION = 250
DEFAULT_ANIMATION_EASING = "cubic-bezier(0.87, 0, 0.13, 1)"
class AccordionComponent(RadixPrimitiveComponent):
def add_style(self) -> Style | None: ...
@ -173,6 +175,8 @@ class AccordionComponent(RadixPrimitiveComponent):
...
class AccordionRoot(AccordionComponent):
def get_event_triggers(self) -> Dict[str, Any]: ...
def add_style(self): ...
@overload
@classmethod
def create( # type: ignore
@ -196,12 +200,15 @@ class AccordionRoot(AccordionComponent):
Literal["vertical", "horizontal"],
]
] = None,
variant: Optional[
radius: Optional[
Union[
Var[Literal["classic", "soft", "surface", "outline", "ghost"]],
Literal["classic", "soft", "surface", "outline", "ghost"],
Var[Literal["none", "small", "medium", "large", "full"]],
Literal["none", "small", "medium", "large", "full"],
]
] = None,
duration: Optional[Union[Var[int], int]] = None,
easing: Optional[Union[Var[str], str]] = None,
show_dividers: Optional[Union[Var[bool], bool]] = None,
color_scheme: Optional[
Union[
Var[
@ -264,6 +271,12 @@ class AccordionRoot(AccordionComponent):
],
]
] = 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,
@ -321,7 +334,7 @@ class AccordionRoot(AccordionComponent):
] = None,
**props
) -> "AccordionRoot":
"""Create the Accordion root component.
"""Create the component.
Args:
*children: The children of the component.
@ -332,8 +345,12 @@ 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 component.
radius: The radius of the accordion corners.
duration: The time in milliseconds to animate open and close
easing: The easing function to use for the animation.
show_dividers: Whether to show divider lines between items.
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.
@ -341,14 +358,12 @@ 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 properties of the component.
**props: The props of the component.
Returns:
The Accordion root Component.
The component.
"""
...
def get_event_triggers(self) -> Dict[str, Any]: ...
def add_style(self): ...
class AccordionItem(AccordionComponent):
@overload
@ -656,8 +671,6 @@ class AccordionHeader(AccordionComponent):
...
def add_style(self) -> Style | None: ...
cubic_bezier = "cubic-bezier(0.87, 0, 0.13, 1)"
class AccordionTrigger(AccordionComponent):
@overload
@classmethod

View File

@ -44,7 +44,7 @@ class FormRoot(FormComponent, HTMLForm):
Returns:
The style of the component.
"""
return Style({"width": "260px"})
return Style({"width": "100%"})
class FormField(FormComponent):

View File

@ -23,7 +23,8 @@ from typing import Literal, get_args
from reflex.components.component import BaseComponent
from reflex.components.core.cond import Cond, color_mode_cond, cond
from reflex.components.lucide.icon import Icon
from reflex.style import color_mode, toggle_color_mode
from reflex.components.radix.themes.components.switch import Switch
from reflex.style import LIGHT_COLOR_MODE, color_mode, toggle_color_mode
from reflex.utils import console
from reflex.vars import BaseVar, Var
@ -143,11 +144,34 @@ class ColorModeIconButton(IconButton):
)
class ColorModeSwitch(Switch):
"""Switch for toggling light / dark mode via toggle_color_mode."""
@classmethod
def create(cls, *children, **props):
"""Create a switch component bound to color_mode.
Args:
*children: The children of the component.
**props: The props to pass to the component.
Returns:
The switch component.
"""
return Switch.create(
*children,
checked=color_mode != LIGHT_COLOR_MODE,
on_change=toggle_color_mode,
**props,
)
class ColorModeNamespace(BaseVar):
"""Namespace for color mode components."""
icon = staticmethod(ColorModeIcon.create)
button = staticmethod(ColorModeIconButton.create)
switch = staticmethod(ColorModeSwitch.create)
color_mode_var_and_namespace = ColorModeNamespace(**dataclasses.asdict(color_mode))

View File

@ -12,7 +12,8 @@ from typing import Literal, get_args
from reflex.components.component import BaseComponent
from reflex.components.core.cond import Cond, color_mode_cond, cond
from reflex.components.lucide.icon import Icon
from reflex.style import color_mode, toggle_color_mode
from reflex.components.radix.themes.components.switch import Switch
from reflex.style import LIGHT_COLOR_MODE, color_mode, toggle_color_mode
from reflex.utils import console
from reflex.vars import BaseVar, Var
from .components.icon_button import IconButton
@ -362,8 +363,184 @@ class ColorModeIconButton(IconButton):
"""
...
class ColorModeSwitch(Switch):
@overload
@classmethod
def create( # type: ignore
cls,
*children,
as_child: Optional[Union[Var[bool], bool]] = None,
default_checked: Optional[Union[Var[bool], bool]] = None,
checked: Optional[Union[Var[bool], bool]] = None,
disabled: Optional[Union[Var[bool], bool]] = None,
required: Optional[Union[Var[bool], bool]] = None,
name: Optional[Union[Var[str], str]] = None,
value: Optional[Union[Var[str], str]] = None,
size: Optional[
Union[Var[Literal["1", "2", "3"]], Literal["1", "2", "3"]]
] = None,
variant: Optional[
Union[
Var[Literal["classic", "surface", "soft"]],
Literal["classic", "surface", "soft"],
]
] = 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,
high_contrast: Optional[Union[Var[bool], bool]] = None,
radius: Optional[
Union[
Var[Literal["none", "small", "full"]], Literal["none", "small", "full"]
]
] = 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_change: 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
) -> "ColorModeSwitch":
"""Create a switch component bound to color_mode.
Args:
*children: The children of the component.
as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
default_checked: Whether the switch is checked by default
checked: Whether the switch is checked
disabled: If true, prevent the user from interacting with the switch
required: If true, the user must interact with the switch to submit the form
name: The name of the switch (when submitting a form)
value: The value associated with the "on" position
size: Switch size "1" - "4"
variant: Variant of switch: "classic" | "surface" | "soft"
color_scheme: Override theme color for switch
high_contrast: Whether to render the switch with higher contrast color against background
radius: Override theme radius for switch: "none" | "small" | "full"
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 to pass to the component.
Returns:
The switch component.
"""
...
class ColorModeNamespace(BaseVar):
icon = staticmethod(ColorModeIcon.create)
button = staticmethod(ColorModeIconButton.create)
switch = staticmethod(ColorModeSwitch.create)
color_mode_var_and_namespace = ColorModeNamespace(**dataclasses.asdict(color_mode))

View File

@ -20,7 +20,6 @@ from reflex import constants
from reflex.config import get_config
from reflex.constants import CustomComponents
from reflex.utils import console
from reflex.utils.pyi_generator import PyiGenerator
config = get_config()
custom_components_cli = typer.Typer()
@ -438,6 +437,8 @@ def _run_commands_in_subprocess(cmds: list[str]) -> bool:
def _make_pyi_files():
"""Create pyi files for the custom component."""
from reflex.utils.pyi_generator import PyiGenerator
package_name = _get_package_config()["project"]["name"]
for dir, _, _ in os.walk(f"./{package_name}"):

View File

@ -180,12 +180,15 @@ class Style(dict):
style_dict: The style dictionary.
kwargs: Other key value pairs to apply to the dict update.
"""
if kwargs:
style_dict = {**(style_dict or {}), **kwargs}
if not isinstance(style_dict, Style):
converted_dict = type(self)(style_dict)
else:
converted_dict = style_dict
if kwargs:
if converted_dict is None:
converted_dict = type(self)(kwargs)
else:
converted_dict.update(kwargs)
# Combine our VarData with that of any Vars in the style_dict that was passed.
self._var_data = VarData.merge(self._var_data, converted_dict._var_data)
super().update(converted_dict)

View File

@ -181,7 +181,12 @@ def get_install_package_manager() -> str | None:
Returns:
The path to the package manager.
"""
if constants.IS_WINDOWS and not is_windows_bun_supported():
if (
constants.IS_WINDOWS
and not is_windows_bun_supported()
or windows_check_onedrive_in_path()
or windows_npm_escape_hatch()
):
return get_package_manager()
return get_config().bun_path
@ -199,6 +204,24 @@ def get_package_manager() -> str | None:
return npm_path
def windows_check_onedrive_in_path() -> bool:
"""For windows, check if oneDrive is present in the project dir path.
Returns:
If oneDrive is in the path of the project directory.
"""
return "onedrive" in str(Path.cwd()).lower()
def windows_npm_escape_hatch() -> bool:
"""For windows, if the user sets REFLEX_USE_NPM, use npm instead of bun.
Returns:
If the user has set REFLEX_USE_NPM.
"""
return os.environ.get("REFLEX_USE_NPM", "").lower() in ["true", "1", "yes"]
def get_app(reload: bool = False) -> ModuleType:
"""Get the app module based on the default config.
@ -737,10 +760,17 @@ def install_bun():
Raises:
FileNotFoundError: If required packages are not found.
"""
if constants.IS_WINDOWS and not is_windows_bun_supported():
console.warn(
"Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
)
win_supported = is_windows_bun_supported()
one_drive_in_path = windows_check_onedrive_in_path()
if constants.IS_WINDOWS and not win_supported or one_drive_in_path:
if not win_supported:
console.warn(
"Bun for Windows is currently only available for x86 64-bit Windows. Installation will fall back on npm."
)
if one_drive_in_path:
console.warn(
"Creating project directories in OneDrive is not recommended for bun usage on windows. This will fallback to npm."
)
# Skip if bun is already installed.
if os.path.exists(get_config().bun_path) and get_bun_version() == version.parse(
@ -843,6 +873,7 @@ def install_frontend_packages(packages: set[str], config: Config):
if not constants.IS_WINDOWS
or constants.IS_WINDOWS
and is_windows_bun_supported()
and not windows_check_onedrive_in_path()
else None
)
processes.run_process_with_fallback(
@ -929,6 +960,11 @@ def needs_reinit(frontend: bool = True) -> bool:
console.warn(
f"""On Python < 3.12, `uvicorn==0.20.0` is recommended for improved hot reload times. Found {uvi_ver} instead."""
)
if windows_check_onedrive_in_path():
console.warn(
"Creating project directories in OneDrive may lead to performance issues. For optimal performance, It is recommended to avoid using OneDrive for your reflex app."
)
# No need to reinitialize if the app is already initialized.
return False

View File

@ -2,17 +2,11 @@ from typing import Dict, List, Set, Tuple, Union
import pytest
from reflex.components import box, foreach, text
from reflex.components.core import Foreach
from reflex.components import box, el, foreach, text
from reflex.components.core.foreach import Foreach, ForeachRenderError, ForeachVarError
from reflex.state import BaseState
from reflex.vars import Var
try:
# When pydantic v2 is installed
from pydantic.v1 import ValidationError # type: ignore
except ImportError:
from pydantic import ValidationError
class ForEachState(BaseState):
"""A state for testing the ForEach component."""
@ -84,12 +78,12 @@ def display_nested_color_with_shades_v2(color):
def display_color_tuple(color):
assert color._var_type == str
return box(text(color, "tuple"))
return box(text(color))
def display_colors_set(color):
assert color._var_type == str
return box(text(color, "set"))
return box(text(color))
def display_nested_list_element(element: Var[str], index: Var[int]):
@ -100,7 +94,7 @@ def display_nested_list_element(element: Var[str], index: Var[int]):
def display_color_index_tuple(color):
assert color._var_type == Union[int, str]
return box(text(color, "index_tuple"))
return box(text(color))
seen_index_vars = set()
@ -215,24 +209,46 @@ def test_foreach_render(state_var, render_fn, render_dict):
# Make sure the index vars are unique.
arg_index = rend["arg_index"]
assert isinstance(arg_index, Var)
assert arg_index._var_name not in seen_index_vars
assert arg_index._var_type == int
seen_index_vars.add(arg_index._var_name)
def test_foreach_bad_annotations():
"""Test that the foreach component raises a TypeError if the iterable is of type Any."""
with pytest.raises(TypeError):
"""Test that the foreach component raises a ForeachVarError if the iterable is of type Any."""
with pytest.raises(ForeachVarError):
Foreach.create(
ForEachState.bad_annotation_list, # type: ignore
ForEachState.bad_annotation_list,
lambda sublist: Foreach.create(sublist, lambda color: text(color)),
)
def test_foreach_no_param_in_signature():
"""Test that the foreach component raises a TypeError if no parameters are passed."""
with pytest.raises(ValidationError):
"""Test that the foreach component raises a ForeachRenderError if no parameters are passed."""
with pytest.raises(ForeachRenderError):
Foreach.create(
ForEachState.colors_list, # type: ignore
ForEachState.colors_list,
lambda: text("color"),
)
def test_foreach_too_many_params_in_signature():
"""Test that the foreach component raises a ForeachRenderError if too many parameters are passed."""
with pytest.raises(ForeachRenderError):
Foreach.create(
ForEachState.colors_list,
lambda color, index, extra: text(color),
)
def test_foreach_component_styles():
"""Test that the foreach component works with global component styles."""
component = el.div(
foreach(
ForEachState.colors_list,
display_color,
)
)
component._add_style_recursive({box: {"color": "red"}})
assert 'css={{"color": "red"}}' in str(component)

View File

@ -1951,3 +1951,73 @@ def test_component_add_custom_code():
"const custom_code5 = 46",
"const custom_code6 = 47",
}
def test_add_style_embedded_vars(test_state: BaseState):
"""Test that add_style works with embedded vars when returning a plain dict.
Args:
test_state: A test state.
"""
v0 = Var.create_safe("parent")._replace(
merge_var_data=VarData(hooks={"useParent": None}), # type: ignore
)
v1 = rx.color("plum", 10)
v2 = Var.create_safe("text")._replace(
merge_var_data=VarData(hooks={"useText": None}), # type: ignore
)
class ParentComponent(Component):
def add_style(self):
return Style(
{
"fake_parent": v0,
}
)
class StyledComponent(ParentComponent):
tag = "StyledComponent"
def add_style(self):
return {
"color": v1,
"fake": v2,
"margin": f"{test_state.num}%",
}
page = rx.vstack(StyledComponent.create())
page._add_style_recursive(Style())
assert (
"const test_state = useContext(StateContexts.test_state)"
in page._get_all_hooks_internal()
)
assert "useText" in page._get_all_hooks_internal()
assert "useParent" in page._get_all_hooks_internal()
assert (
str(page).count(
'css={{"fakeParent": "parent", "color": "var(--plum-10)", "fake": "text", "margin": `${test_state.num}%`}}'
)
== 1
)
def test_add_style_foreach():
class StyledComponent(Component):
tag = "StyledComponent"
ix: Var[int]
def add_style(self):
return Style({"color": "red"})
page = rx.vstack(rx.foreach(Var.range(3), lambda i: StyledComponent.create(i)))
page._add_style_recursive(Style())
# Expect only a single child of the foreach on the python side
assert len(page.children[0].children) == 1
# Expect the style to be added to the child of the foreach
assert 'css={{"color": "red"}}' in str(page.children[0].children[0])
# Expect only one instance of this CSS dict in the rendered page
assert str(page).count('css={{"color": "red"}}') == 1

View File

@ -8,7 +8,7 @@ import reflex as rx
from reflex import style
from reflex.components.component import evaluate_style_namespaces
from reflex.style import Style
from reflex.vars import Var
from reflex.vars import Var, VarData
test_style = [
({"a": 1}, {"a": 1}),
@ -503,3 +503,25 @@ def test_evaluate_style_namespaces():
assert rx.text.__call__ not in style_dict
style_dict = evaluate_style_namespaces(style_dict) # type: ignore
assert rx.text.__call__ in style_dict
def test_style_update_with_var_data():
"""Test that .update with a Style containing VarData works."""
red_var = Var.create_safe("red")._replace(
merge_var_data=VarData(hooks={"const red = true": None}), # type: ignore
)
blue_var = Var.create_safe("blue", _var_is_local=False)._replace(
merge_var_data=VarData(hooks={"const blue = true": None}), # type: ignore
)
s1 = Style(
{
"color": red_var,
}
)
s2 = Style()
s2.update(s1, background_color=f"{blue_var}ish")
assert s2 == {"color": "red", "backgroundColor": "`${blue}ish`"}
assert s2._var_data is not None
assert "const red = true" in s2._var_data.hooks
assert "const blue = true" in s2._var_data.hooks