123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- """Handle styling."""
- from __future__ import annotations
- from typing import Any
- from reflex import constants
- from reflex.event import EventChain
- from reflex.utils import format
- from reflex.utils.imports import ImportVar
- from reflex.vars import BaseVar, Var, VarData
- VarData.update_forward_refs() # Ensure all type definitions are resolved
- # Reference the global ColorModeContext
- color_mode_var_data = VarData( # type: ignore
- imports={
- f"/{constants.Dirs.CONTEXTS_PATH}": {ImportVar(tag="ColorModeContext")},
- "react": {ImportVar(tag="useContext")},
- },
- hooks={
- f"const [ {constants.ColorMode.NAME}, {constants.ColorMode.TOGGLE} ] = useContext(ColorModeContext)",
- },
- )
- # Var resolves to the current color mode for the app ("light" or "dark")
- color_mode = BaseVar(
- _var_name=constants.ColorMode.NAME,
- _var_type="str",
- _var_data=color_mode_var_data,
- )
- # Var resolves to a function invocation that toggles the color mode
- toggle_color_mode = BaseVar(
- _var_name=constants.ColorMode.TOGGLE,
- _var_type=EventChain,
- _var_data=color_mode_var_data,
- )
- breakpoints = ["0", "30em", "48em", "62em", "80em", "96em"]
- def media_query(breakpoint_index: int):
- """Create a media query selector.
- Args:
- breakpoint_index: The index of the breakpoint to use.
- Returns:
- The media query selector used as a key in emotion css dict.
- """
- return f"@media screen and (min-width: {breakpoints[breakpoint_index]})"
- def convert_item(style_item: str | Var) -> tuple[str, 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.
- """
- if isinstance(style_item, Var):
- # If the value is a Var, extract the var_data and cast as str.
- return str(style_item), style_item._var_data
- # Otherwise, convert to Var to collapse VarData encoded in f-string.
- new_var = Var.create(style_item)
- if new_var is not None and new_var._var_data:
- # The wrapped backtick is used to identify the Var for interpolation.
- return f"`{str(new_var)}`", new_var._var_data
- return style_item, None
- def convert_list(
- responsive_list: list[str | dict | Var],
- ) -> tuple[list[str | 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):
- """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 = {}
- for key, value in style_dict.items():
- key = format.to_camel_case(key)
- if isinstance(value, dict):
- # Recursively format nested style dictionaries.
- out[key], new_var_data = convert(value)
- elif isinstance(value, list):
- # Responsive value is a list of dict or value
- out[key], new_var_data = convert_list(value)
- else:
- out[key], new_var_data = convert_item(value)
- # Combine all the collected VarData instances.
- var_data = VarData.merge(var_data, new_var_data)
- return out, var_data
- class Style(dict):
- """A style dictionary."""
- def __init__(self, style_dict: dict | None = None):
- """Initialize the style.
- Args:
- style_dict: The style dictionary.
- """
- style_dict, self._var_data = convert(style_dict or {})
- 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 kwargs:
- style_dict = {**(style_dict or {}), **kwargs}
- if not isinstance(style_dict, Style):
- converted_dict = type(self)(style_dict)
- else:
- converted_dict = style_dict
- # 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 = Var.create(value)
- if _var is not None:
- # Carry the imports/hooks when setting a Var as a value.
- self._var_data = VarData.merge(self._var_data, _var._var_data)
- super().__setitem__(key, value)
- 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).
- Returns:
- A self-referential pseudo selector key (&:hover).
- """
- prefix = None
- if key.startswith("_"):
- # Handle pseudo selectors in chakra style format.
- 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]) -> dict[str, Any] | None:
- """Convert the style to an emotion-compatible CSS-in-JS dict.
- Args:
- style_dict: The style dict to convert.
- Returns:
- The emotion dict.
- """
- emotion_style = {}
- for orig_key, value in style_dict.items():
- key = _format_emotion_style_pseudo_selector(orig_key)
- if isinstance(value, list):
- # Apply media queries from responsive value list.
- mbps = {
- media_query(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:
- return emotion_style
|