layout.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. """To experiment with layout component, move them to reflex/components later."""
  2. from __future__ import annotations
  3. from typing import Any, List
  4. from reflex import color, cond
  5. from reflex.components.base.fragment import Fragment
  6. from reflex.components.component import Component, ComponentNamespace, MemoizationLeaf
  7. from reflex.components.radix.primitives.drawer import DrawerRoot, drawer
  8. from reflex.components.radix.themes.components.icon_button import IconButton
  9. from reflex.components.radix.themes.layout.box import Box
  10. from reflex.components.radix.themes.layout.container import Container
  11. from reflex.components.radix.themes.layout.stack import HStack
  12. from reflex.event import call_script
  13. from reflex.experimental import hooks
  14. from reflex.state import ComponentState
  15. from reflex.style import Style
  16. from reflex.vars.base import Var
  17. class Sidebar(Box, MemoizationLeaf):
  18. """A component that renders the sidebar."""
  19. @classmethod
  20. def create(cls, *children, **props):
  21. """Create the sidebar component.
  22. Args:
  23. children: The children components.
  24. props: The properties of the sidebar.
  25. Returns:
  26. The sidebar component.
  27. """
  28. # props.setdefault("border_right", f"1px solid {color('accent', 12)}")
  29. # props.setdefault("background_color", color("accent", 1))
  30. # props.setdefault("width", "20vw")
  31. # props.setdefault("height", "100vh")
  32. # props.setdefault("position", "fixed")
  33. return super().create(
  34. Box.create(*children, **props), # sidebar for content
  35. Box.create(width=props.get("width")), # spacer for layout
  36. )
  37. def add_style(self) -> dict[str, Any] | None:
  38. """Add style to the component.
  39. Returns:
  40. The style of the component.
  41. """
  42. sidebar: Component = self.children[-2] # type: ignore
  43. spacer: Component = self.children[-1] # type: ignore
  44. open = (
  45. self.State.open # type: ignore
  46. if self.State
  47. else Var(_js_expr="open")
  48. )
  49. sidebar.style["display"] = spacer.style["display"] = cond(open, "block", "none")
  50. return Style(
  51. {
  52. "position": "fixed",
  53. "border_right": f"1px solid {color('accent', 12)}",
  54. "background_color": color("accent", 1),
  55. "width": "20vw",
  56. "height": "100vh",
  57. }
  58. )
  59. def add_hooks(self) -> List[Var]:
  60. """Get the hooks to render.
  61. Returns:
  62. The hooks for the sidebar.
  63. """
  64. return [hooks.useState("open", "true")] if not self.State else []
  65. class StatefulSidebar(ComponentState):
  66. """Bind a state to a sidebar component."""
  67. open: bool = True
  68. def toggle(self):
  69. """Toggle the sidebar."""
  70. self.open = not self.open
  71. @classmethod
  72. def get_component(cls, *children, **props):
  73. """Get the stateful sidebar component.
  74. Args:
  75. children: The children components.
  76. props: The properties of the sidebar.
  77. Returns:
  78. The stateful sidebar component.
  79. """
  80. return Sidebar.create(*children, **props)
  81. class DrawerSidebar(DrawerRoot):
  82. """A component that renders a drawer sidebar."""
  83. @classmethod
  84. def create(cls, *children, **props):
  85. """Create the sidebar component.
  86. Args:
  87. children: The children components.
  88. props: The properties of the sidebar.
  89. Returns:
  90. The drawer sidebar component.
  91. """
  92. direction = props.pop("direction", "left")
  93. props.setdefault("border_right", f"1px solid {color('accent', 12)}")
  94. props.setdefault("background_color", color("accent", 1))
  95. props.setdefault("width", "20vw")
  96. props.setdefault("height", "100vh")
  97. return super().create(
  98. drawer.trigger(
  99. IconButton.create(
  100. "arrow-right-from-line",
  101. background_color="transparent",
  102. ),
  103. position="absolute",
  104. top="15",
  105. left="15",
  106. ),
  107. drawer.portal(
  108. drawer.content(
  109. *children,
  110. **props,
  111. )
  112. ),
  113. direction=direction,
  114. )
  115. sidebar_trigger_style = {
  116. "position": "fixed",
  117. "z_index": "15",
  118. "color": color("accent", 12),
  119. "background_color": "transparent",
  120. "padding": "0",
  121. }
  122. class SidebarTrigger(Fragment):
  123. """A component that renders the sidebar trigger."""
  124. @classmethod
  125. def create(cls, sidebar: Component, **props):
  126. """Create the sidebar trigger component.
  127. Args:
  128. sidebar: The sidebar component.
  129. props: The properties of the sidebar trigger.
  130. Returns:
  131. The sidebar trigger component.
  132. """
  133. trigger_props = {**props, **sidebar_trigger_style}
  134. inner_sidebar: Component = sidebar.children[0] # type: ignore
  135. sidebar_width = inner_sidebar.style.get("width")
  136. if sidebar.State:
  137. open, toggle = sidebar.State.open, sidebar.State.toggle # type: ignore
  138. else:
  139. open, toggle = (
  140. Var(_js_expr="open"),
  141. call_script(Var(_js_expr="setOpen(!open)")),
  142. )
  143. trigger_props["left"] = cond(open, f"calc({sidebar_width} - 32px)", "0")
  144. trigger = cond(
  145. open,
  146. IconButton.create(
  147. "arrow-left-from-line",
  148. on_click=toggle,
  149. **trigger_props,
  150. ),
  151. IconButton.create(
  152. "arrow-right-from-line",
  153. on_click=toggle,
  154. **trigger_props,
  155. ),
  156. )
  157. return super().create(trigger)
  158. class Layout(Box):
  159. """A component that renders the layout."""
  160. @classmethod
  161. def create(
  162. cls,
  163. *content: Component,
  164. sidebar: Component | None = None,
  165. **props,
  166. ):
  167. """Create the layout component.
  168. Args:
  169. content: The content component.
  170. sidebar: The sidebar component.
  171. props: The properties of the layout.
  172. Returns:
  173. The layout component.
  174. """
  175. layout_root = HStack.create
  176. if sidebar is None:
  177. return Container.create(*content, **props)
  178. if isinstance(sidebar, DrawerSidebar):
  179. return super().create(
  180. sidebar,
  181. Container.create(*content, height="100%"),
  182. **props,
  183. width="100vw",
  184. min_height="100vh",
  185. )
  186. if not isinstance(sidebar, Sidebar):
  187. sidebar = Sidebar.create(sidebar)
  188. # Add the sidebar trigger to the sidebar as first child to not mess up the rendering.
  189. sidebar.children.insert(0, SidebarTrigger.create(sidebar))
  190. return super().create(
  191. layout_root(
  192. sidebar,
  193. Container.create(*content, height="100%"),
  194. **props,
  195. width="100vw",
  196. min_height="100vh",
  197. )
  198. )
  199. class LayoutNamespace(ComponentNamespace):
  200. """Namespace for layout components."""
  201. drawer_sidebar = staticmethod(DrawerSidebar.create)
  202. stateful_sidebar = staticmethod(StatefulSidebar.create)
  203. sidebar = staticmethod(Sidebar.create)
  204. __call__ = staticmethod(Layout.create)
  205. layout = LayoutNamespace()