123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- """Handle styling."""
- from __future__ import annotations
- from collections.abc import Mapping
- from typing import Any, Literal
- from reflex import constants
- from reflex.components.core.breakpoints import Breakpoints, breakpoints_values
- from reflex.event import EventChain, EventHandler, EventSpec, run_script
- from reflex.utils import format
- from reflex.utils.exceptions import ReflexError
- from reflex.utils.imports import ImportVar
- from reflex.utils.types import typehint_issubclass
- from reflex.vars import VarData
- from reflex.vars.base import LiteralVar, Var
- from reflex.vars.function import FunctionVar
- from reflex.vars.object import ObjectVar
- SYSTEM_COLOR_MODE: str = "system"
- LIGHT_COLOR_MODE: str = "light"
- DARK_COLOR_MODE: str = "dark"
- LiteralColorMode = Literal["system", "light", "dark"]
- # Reference the global ColorModeContext
- color_mode_imports = {
- f"$/{constants.Dirs.CONTEXTS_PATH}": [ImportVar(tag="ColorModeContext")],
- "react": [ImportVar(tag="useContext")],
- }
- def _color_mode_var(_js_expr: str, _var_type: type = str) -> Var:
- """Create a Var that destructs the _js_expr from ColorModeContext.
- Args:
- _js_expr: The name of the variable to get from ColorModeContext.
- _var_type: The type of the Var.
- Returns:
- The Var that resolves to the color mode.
- """
- return Var(
- _js_expr=_js_expr,
- _var_type=_var_type,
- _var_data=VarData(
- imports=color_mode_imports,
- hooks={f"const {{ {_js_expr} }} = useContext(ColorModeContext)": None},
- ),
- ).guess_type()
- def set_color_mode(
- new_color_mode: LiteralColorMode | Var[LiteralColorMode],
- ) -> EventSpec:
- """Create an EventSpec Var that sets the color mode to a specific value.
- Note: `set_color_mode` is not a real event and cannot be triggered from a
- backend event handler.
- Args:
- new_color_mode: The color mode to set.
- Returns:
- The EventSpec Var that can be passed to an event trigger.
- """
- base_setter = _color_mode_var(
- _js_expr=constants.ColorMode.SET,
- ).to(FunctionVar)
- return run_script(
- base_setter.call(new_color_mode),
- )
- # Var resolves to the current color mode for the app ("light", "dark" or "system")
- color_mode = _color_mode_var(_js_expr=constants.ColorMode.NAME)
- # Var resolves to the resolved color mode for the app ("light" or "dark")
- resolved_color_mode = _color_mode_var(_js_expr=constants.ColorMode.RESOLVED_NAME)
- # Var resolves to a function invocation that toggles the color mode
- toggle_color_mode = _color_mode_var(
- _js_expr=constants.ColorMode.TOGGLE,
- _var_type=EventChain,
- )
- STYLE_PROP_SHORTHAND_MAPPING = {
- "paddingX": ("paddingInlineStart", "paddingInlineEnd"),
- "paddingY": ("paddingTop", "paddingBottom"),
- "marginX": ("marginInlineStart", "marginInlineEnd"),
- "marginY": ("marginTop", "marginBottom"),
- "bg": ("background",),
- "bgColor": ("backgroundColor",),
- # Radix components derive their font from this CSS var, not inherited from body or class.
- "fontFamily": ("fontFamily", "--default-font-family"),
- }
- def media_query(breakpoint_expr: str):
- """Create a media query selector.
- Args:
- breakpoint_expr: The CSS expression representing the breakpoint.
- Returns:
- The media query selector used as a key in emotion css dict.
- """
- return f"@media screen and (min-width: {breakpoint_expr})"
- def convert_item(
- style_item: int | str | Var,
- ) -> tuple[str | Var, VarData | None]:
- """Format a single value in a style dictionary.
- Args:
- style_item: The style item to format.
- Returns:
- The formatted style item and any associated VarData.
- Raises:
- ReflexError: If an EventHandler is used as a style value
- """
- if isinstance(style_item, EventHandler):
- raise ReflexError(
- "EventHandlers cannot be used as style values. "
- "Please use a Var or a literal value."
- )
- if isinstance(style_item, Var):
- return style_item, style_item._get_all_var_data()
- # Otherwise, convert to Var to collapse VarData encoded in f-string.
- new_var = LiteralVar.create(style_item)
- var_data = new_var._get_all_var_data() if new_var is not None else None
- return new_var, var_data
- def convert_list(
- responsive_list: list[str | dict | Var],
- ) -> tuple[list[str | dict[str, Var | list | dict]], VarData | None]:
- """Format a responsive value list.
- Args:
- responsive_list: The raw responsive value list (one value per breakpoint).
- Returns:
- The recursively converted responsive value list and any associated VarData.
- """
- converted_value = []
- item_var_datas = []
- for responsive_item in responsive_list:
- if isinstance(responsive_item, dict):
- # Recursively format nested style dictionaries.
- item, item_var_data = convert(responsive_item)
- else:
- item, item_var_data = convert_item(responsive_item)
- converted_value.append(item)
- item_var_datas.append(item_var_data)
- return converted_value, VarData.merge(*item_var_datas)
- def convert(
- style_dict: dict[str, Var | dict | list | str],
- ) -> tuple[dict[str, str | list | dict], VarData | None]:
- """Format a style dictionary.
- Args:
- style_dict: The style dictionary to format.
- Returns:
- The formatted style dictionary.
- """
- var_data = None # Track import/hook data from any Vars in the style dict.
- out = {}
- def update_out_dict(
- return_value: Var | dict | list | str, keys_to_update: tuple[str, ...]
- ):
- for k in keys_to_update:
- out[k] = return_value
- for key, value in style_dict.items():
- keys = (
- format_style_key(key)
- if not isinstance(value, (dict, ObjectVar, list))
- or (
- isinstance(value, Breakpoints)
- and all(not isinstance(v, dict) for v in value.values())
- )
- or (isinstance(value, list) and all(not isinstance(v, dict) for v in value))
- or (
- isinstance(value, ObjectVar)
- and not typehint_issubclass(value._var_type, Mapping)
- )
- else (key,)
- )
- if isinstance(value, Var):
- return_val = value
- new_var_data = value._get_all_var_data()
- update_out_dict(return_val, keys)
- elif isinstance(value, dict):
- # Recursively format nested style dictionaries.
- return_val, new_var_data = convert(value)
- update_out_dict(return_val, keys)
- elif isinstance(value, list):
- # Responsive value is a list of dict or value
- return_val, new_var_data = convert_list(value)
- update_out_dict(return_val, keys)
- else:
- return_val, new_var_data = convert_item(value)
- update_out_dict(return_val, keys)
- # Combine all the collected VarData instances.
- var_data = VarData.merge(var_data, new_var_data)
- if isinstance(style_dict, Breakpoints):
- out = Breakpoints(out).factorize()
- return out, var_data
- def format_style_key(key: str) -> tuple[str, ...]:
- """Convert style keys to camel case and convert shorthand
- styles names to their corresponding css names.
- Args:
- key: The style key to convert.
- Returns:
- Tuple of css style names corresponding to the key provided.
- """
- if key.startswith("--"):
- return (key,)
- key = format.to_camel_case(key)
- return STYLE_PROP_SHORTHAND_MAPPING.get(key, (key,))
- EMPTY_VAR_DATA = VarData()
- class Style(dict[str, Any]):
- """A style dictionary."""
- def __init__(self, style_dict: dict[str, Any] | 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
- if style_dict:
- style_dict, self._var_data = convert(style_dict)
- else:
- self._var_data = EMPTY_VAR_DATA
- super().__init__(style_dict)
- def update(self, style_dict: dict | None, **kwargs):
- """Update the style.
- Args:
- style_dict: The style dictionary.
- kwargs: Other key value pairs to apply to the dict update.
- """
- 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)
- def __setitem__(self, key: str, value: Any):
- """Set an item in the style.
- Args:
- key: The key to set.
- value: The value to set.
- """
- # Create a Var to collapse VarData encoded in f-string.
- _var = LiteralVar.create(value)
- if _var is not None:
- # Carry the imports/hooks when setting a Var as a value.
- self._var_data = VarData.merge(
- getattr(self, "_var_data", None), _var._get_all_var_data()
- )
- super().__setitem__(key, value)
- def __or__(self, other: Style | dict) -> Style:
- """Combine two styles.
- Args:
- other: The other style to combine.
- Returns:
- The combined style.
- """
- other_var_data = None
- if not isinstance(other, Style):
- other_dict, other_var_data = convert(other)
- else:
- other_dict, other_var_data = other, other._var_data
- new_style = Style(super().__or__(other_dict))
- if self._var_data or other_var_data:
- new_style._var_data = VarData.merge(self._var_data, other_var_data)
- return new_style
- def _format_emotion_style_pseudo_selector(key: str) -> str:
- """Format a pseudo selector for emotion CSS-in-JS.
- Args:
- key: Underscore-prefixed or colon-prefixed pseudo selector key (_hover/:hover).
- Returns:
- A self-referential pseudo selector key (&:hover).
- """
- prefix = None
- if key.startswith("_"):
- prefix = "&:"
- key = key[1:]
- if key.startswith(":"):
- # Handle pseudo selectors and elements in native format.
- prefix = "&"
- if prefix is not None:
- return prefix + format.to_kebab_case(key)
- return key
- def format_as_emotion(style_dict: dict[str, Any]) -> Style | None:
- """Convert the style to an emotion-compatible CSS-in-JS dict.
- Args:
- style_dict: The style dict to convert.
- Returns:
- The emotion style dict.
- """
- _var_data = style_dict._var_data if isinstance(style_dict, Style) else None
- emotion_style = Style()
- for orig_key, value in style_dict.items():
- key = _format_emotion_style_pseudo_selector(orig_key)
- if isinstance(value, (Breakpoints, list)):
- if isinstance(value, Breakpoints):
- mbps = {
- media_query(bp): (
- bp_value if isinstance(bp_value, dict) else {key: bp_value}
- )
- for bp, bp_value in value.items()
- }
- else:
- # Apply media queries from responsive value list.
- mbps = {
- media_query([0, *breakpoints_values][bp]): (
- bp_value if isinstance(bp_value, dict) else {key: bp_value}
- )
- for bp, bp_value in enumerate(value)
- }
- if key.startswith("&:"):
- emotion_style[key] = mbps
- else:
- for mq, style_sub_dict in mbps.items():
- emotion_style.setdefault(mq, {}).update(style_sub_dict)
- elif isinstance(value, dict):
- # Recursively format nested style dictionaries.
- emotion_style[key] = format_as_emotion(value)
- else:
- emotion_style[key] = value
- if emotion_style:
- if _var_data is not None:
- emotion_style._var_data = VarData.merge(emotion_style._var_data, _var_data)
- return emotion_style
- def convert_dict_to_style_and_format_emotion(
- raw_dict: dict[str, Any],
- ) -> dict[str, Any] | None:
- """Convert a dict to a style dict and then format as emotion.
- Args:
- raw_dict: The dict to convert.
- Returns:
- The emotion dict.
- """
- return format_as_emotion(Style(raw_dict))
- STACK_CHILDREN_FULL_WIDTH = {
- "& :where(.rx-Stack)": {
- "width": "100%",
- },
- "& :where(.rx-Stack) > :where( "
- "div:not(.rt-Box, .rx-Upload, .rx-Html),"
- "input, select, textarea, table"
- ")": {
- "width": "100%",
- "flex_shrink": "1",
- },
- }
|