/ libs / ui / src / components / collapsible.rs
collapsible.rs
  1  use crate::use_unique_id;
  2  use dioxus::html::GlobalAttributesExtension;
  3  use dioxus::prelude::*;
  4  use dioxus_primitives::collapsible::{
  5      Collapsible as PrimitiveCollapsible, CollapsibleContent as PrimitiveCollapsibleContent,
  6      CollapsibleTrigger as PrimitiveCollapsibleTrigger,
  7  };
  8  use lucide_dioxus::ChevronDown;
  9  
 10  /// Props for the Collapsible component
 11  #[derive(Props, Clone, PartialEq)]
 12  pub struct CollapsibleProps {
 13      /// Optional additional classes for the collapsible
 14      #[props(default)]
 15      pub class: Option<String>,
 16  
 17      /// Optional ID for the collapsible element
 18      #[props(default)]
 19      pub id: Option<String>,
 20  
 21      /// Child elements
 22      pub children: Element,
 23  }
 24  
 25  /// Styled wrapper for the Collapsible component
 26  #[component]
 27  pub fn Collapsible(props: CollapsibleProps) -> Element {
 28      // Generate unique ID if not provided
 29      let collapsible_id = use_unique_id();
 30      let id_value = use_memo(move || {
 31          props
 32              .id
 33              .clone()
 34              .unwrap_or_else(|| collapsible_id.peek().clone())
 35      });
 36  
 37      let collapsible_classes = vec![
 38          // Base classes - clean container styling
 39          "w-full border border-border rounded-lg",
 40          // Additional classes passed by the user
 41          props.class.as_deref().unwrap_or(""),
 42      ]
 43      .into_iter()
 44      .filter(|s| !s.is_empty())
 45      .collect::<Vec<_>>()
 46      .join(" ");
 47  
 48      rsx! {
 49          PrimitiveCollapsible {
 50              id: id_value.peek().clone(),
 51              class: collapsible_classes,
 52              {props.children}
 53          }
 54      }
 55  }
 56  
 57  /// Props for the CollapsibleTrigger component
 58  #[derive(Props, Clone, PartialEq)]
 59  pub struct CollapsibleTriggerProps {
 60      /// Optional additional classes for the trigger
 61      #[props(default)]
 62      pub class: Option<String>,
 63  
 64      /// Optional ID for the trigger element
 65      #[props(default)]
 66      pub id: Option<String>,
 67  
 68      /// Child elements
 69      pub children: Element,
 70  }
 71  
 72  /// Styled wrapper for the CollapsibleTrigger component
 73  #[component]
 74  pub fn CollapsibleTrigger(props: CollapsibleTriggerProps) -> Element {
 75      // Generate unique ID if not provided
 76      let trigger_id = use_unique_id();
 77      let id_value = use_memo(move || {
 78          props
 79              .id
 80              .clone()
 81              .unwrap_or_else(|| trigger_id.peek().clone())
 82      });
 83  
 84      let trigger_classes = vec![
 85          // Base classes
 86          "flex w-full items-center justify-between py-4 px-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
 87  
 88          // Additional classes passed by the user
 89          props.class.as_deref().unwrap_or(""),
 90      ]
 91      .into_iter()
 92      .filter(|s| !s.is_empty())
 93      .collect::<Vec<_>>()
 94      .join(" ");
 95  
 96      rsx! {
 97          PrimitiveCollapsibleTrigger {
 98              id: id_value.peek().clone(),
 99              class: trigger_classes,
100  
101              {props.children}
102  
103              // Chevron icon
104              ChevronDown {
105                  class: "h-4 w-4 shrink-0 transition-transform duration-200 data-[state=open]:rotate-180"
106              }
107          }
108      }
109  }
110  
111  /// Props for the CollapsibleContent component
112  #[derive(Props, Clone, PartialEq)]
113  pub struct CollapsibleContentProps {
114      /// Optional additional classes for the content
115      #[props(default)]
116      pub class: Option<String>,
117  
118      /// Optional ID for the content element
119      #[props(default)]
120      pub id: Option<String>,
121  
122      /// Child elements
123      pub children: Element,
124  }
125  
126  /// Styled wrapper for the CollapsibleContent component
127  #[component]
128  pub fn CollapsibleContent(props: CollapsibleContentProps) -> Element {
129      // Generate unique ID if not provided
130      let content_id = use_unique_id();
131      let id_value = use_memo(move || {
132          props
133              .id
134              .clone()
135              .unwrap_or_else(|| content_id.peek().clone())
136      });
137  
138      let content_classes = vec![
139          // Base classes - shadcn ui inspired content styling
140          "overflow-hidden text-sm px-4 data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
141  
142          // Additional classes passed by the user
143          props.class.as_deref().unwrap_or(""),
144      ]
145      .into_iter()
146      .filter(|s| !s.is_empty())
147      .collect::<Vec<_>>()
148      .join(" ");
149  
150      rsx! {
151          PrimitiveCollapsibleContent {
152              id: id_value.peek().clone(),
153              class: content_classes,
154              div {
155                  class: "pb-4 pt-0",
156                  {props.children}
157              }
158          }
159      }
160  }