layout.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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 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 = self.State.open if self.State else Var.create("open") # type: ignore
  45. sidebar.style["display"] = spacer.style["display"] = cond(open, "block", "none")
  46. return Style(
  47. {
  48. "position": "fixed",
  49. "border_right": f"1px solid {color('accent', 12)}",
  50. "background_color": color("accent", 1),
  51. "width": "20vw",
  52. "height": "100vh",
  53. }
  54. )
  55. def add_hooks(self) -> List[Var]:
  56. """Get the hooks to render.
  57. Returns:
  58. The hooks for the sidebar.
  59. """
  60. return [hooks.useState("open", "true")] if not self.State else []
  61. class StatefulSidebar(ComponentState):
  62. """Bind a state to a sidebar component."""
  63. open: bool = True
  64. def toggle(self):
  65. """Toggle the sidebar."""
  66. self.open = not self.open
  67. @classmethod
  68. def get_component(cls, *children, **props):
  69. """Get the stateful sidebar component.
  70. Args:
  71. children: The children components.
  72. props: The properties of the sidebar.
  73. Returns:
  74. The stateful sidebar component.
  75. """
  76. return Sidebar.create(*children, **props)
  77. class DrawerSidebar(DrawerRoot):
  78. """A component that renders a drawer sidebar."""
  79. @classmethod
  80. def create(cls, *children, **props):
  81. """Create the sidebar component.
  82. Args:
  83. children: The children components.
  84. props: The properties of the sidebar.
  85. Returns:
  86. The drawer sidebar component.
  87. """
  88. direction = props.pop("direction", "left")
  89. props.setdefault("border_right", f"1px solid {color('accent', 12)}")
  90. props.setdefault("background_color", color("accent", 1))
  91. props.setdefault("width", "20vw")
  92. props.setdefault("height", "100vh")
  93. return super().create(
  94. drawer.trigger(
  95. IconButton.create(
  96. "arrow-right-from-line",
  97. background_color="transparent",
  98. ),
  99. position="absolute",
  100. top="15",
  101. left="15",
  102. ),
  103. drawer.portal(
  104. drawer.content(
  105. *children,
  106. **props,
  107. )
  108. ),
  109. direction=direction,
  110. )
  111. sidebar_trigger_style = {
  112. "position": "fixed",
  113. "z_index": "15",
  114. "color": color("accent", 12),
  115. "background_color": "transparent",
  116. "padding": "0",
  117. }
  118. class SidebarTrigger(Fragment):
  119. """A component that renders the sidebar trigger."""
  120. @classmethod
  121. def create(cls, sidebar: Component, **props):
  122. """Create the sidebar trigger component.
  123. Args:
  124. sidebar: The sidebar component.
  125. props: The properties of the sidebar trigger.
  126. Returns:
  127. The sidebar trigger component.
  128. """
  129. trigger_props = {**props, **sidebar_trigger_style}
  130. inner_sidebar: Component = sidebar.children[0] # type: ignore
  131. sidebar_width = inner_sidebar.style.get("width")
  132. if sidebar.State:
  133. open, toggle = sidebar.State.open, sidebar.State.toggle # type: ignore
  134. else:
  135. open, toggle = Var.create("open"), call_script(Var.create("setOpen(!open)")) # type: ignore
  136. trigger_props["left"] = cond(open, f"calc({sidebar_width} - 32px)", "0")
  137. trigger = cond(
  138. open,
  139. IconButton.create(
  140. "arrow-left-from-line",
  141. on_click=toggle,
  142. **trigger_props,
  143. ),
  144. IconButton.create(
  145. "arrow-right-from-line",
  146. on_click=toggle,
  147. **trigger_props,
  148. ),
  149. )
  150. return super().create(trigger)
  151. class Layout(Box):
  152. """A component that renders the layout."""
  153. @classmethod
  154. def create(
  155. cls,
  156. *content: Component,
  157. sidebar: Component | None = None,
  158. **props,
  159. ):
  160. """Create the layout component.
  161. Args:
  162. content: The content component.
  163. sidebar: The sidebar component.
  164. props: The properties of the layout.
  165. Returns:
  166. The layout component.
  167. """
  168. layout_root = HStack.create
  169. if sidebar is None:
  170. return Container.create(*content, **props)
  171. if isinstance(sidebar, DrawerSidebar):
  172. return super().create(
  173. sidebar,
  174. Container.create(*content, height="100%"),
  175. **props,
  176. width="100vw",
  177. min_height="100vh",
  178. )
  179. if not isinstance(sidebar, Sidebar):
  180. sidebar = Sidebar.create(sidebar)
  181. # Add the sidebar trigger to the sidebar as first child to not mess up the rendering.
  182. sidebar.children.insert(0, SidebarTrigger.create(sidebar))
  183. return super().create(
  184. layout_root(
  185. sidebar,
  186. Container.create(*content, height="100%"),
  187. **props,
  188. width="100vw",
  189. min_height="100vh",
  190. )
  191. )
  192. class LayoutNamespace(ComponentNamespace):
  193. """Namespace for layout components."""
  194. drawer_sidebar = staticmethod(DrawerSidebar.create)
  195. stateful_sidebar = staticmethod(StatefulSidebar.create)
  196. sidebar = staticmethod(Sidebar.create)
  197. __call__ = staticmethod(Layout.create)
  198. layout = LayoutNamespace()