layout.py 7.3 KB

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