1#![doc(
2 html_logo_url = "https://github.com/next-rs/yew-sidebar/assets/62179149/94fb2191-884e-4643-a794-d8e5c459f5d6",
3 html_favicon_url = "https://github.com/next-rs/yew-sidebar/assets/62179149/e0c325e5-16b8-439d-b7c2-48b2ffe8476d"
4)]
5
6use yew::prelude::*;
112use yew_accordion::{Accordion, AccordionButton};
113
114const WIDTH_COLLAPSED: &'static str = "w-16";
115const WIDTH_EXPANDED: &'static str = "w-64";
116const PADDING_COLLAPSED: &'static str = "p-2";
117const PADDING_EXPANDED: &'static str = "p-4";
118const DISPLAY_COLLAPSED: &'static str = "hidden";
119const DISPLAY_EXPANDED: &'static str = "flex";
120const JUSTIFY_CONTENT: &'static str = "justify-start";
121const ALIGN_ITEMS: &'static str = "items-center";
122const HEIGHT: &'static str = "h-screen";
123const BACKGROUND_COLOR: &'static str = "bg-gray-800";
124const FONT: &'static str = "text-black";
125const ICON_COLOR: &'static str = "white";
126const BUTTON_BORDER_RADIUS: &'static str = "rounded";
127const BUTTON_BACKGROUND_COLOR: &'static str = "bg-blue-600";
128const BUTTON_WIDTH: &'static str = "w-12";
129const BUTTON_HEIGHT: &'static str = "h-12";
130const MENU_ITEM: &'static str = "\
131 text-gray-300 \
132 hover:bg-gray-800 \
133 hover:text-white \
134 flex \
135 items-center \
136 space-x-2 \
137 p-1 \
138 rounded \
139 transition duration-300 \
140";
141const LOGO_CLASS: &str = "flex items-center";
142const LOGO_IMG_CLASS: &str = "w-32 md:w-40";
143
144#[derive(Properties, Clone, PartialEq)]
145pub struct SidebarProps {
146 #[prop_or(false)]
148 pub fixed: bool,
149 #[prop_or(false)]
150 pub sider_collapsed: bool,
151 #[prop_or_default]
152 pub menu_items: Vec<MenuItem>,
153 #[prop_or_default]
155 pub toggle_icon_collapsed: Html,
156 #[prop_or_default]
158 pub toggle_icon_expanded: Html,
159
160 #[prop_or(WIDTH_COLLAPSED)]
163 pub width_collapsed: &'static str,
164 #[prop_or(WIDTH_EXPANDED)]
166 pub width_expanded: &'static str,
167 #[prop_or(PADDING_COLLAPSED)]
169 pub padding_collapsed: &'static str,
170 #[prop_or(PADDING_EXPANDED)]
172 pub padding_expanded: &'static str,
173 #[prop_or(DISPLAY_COLLAPSED)]
175 pub display_collapsed: &'static str,
176 #[prop_or(DISPLAY_EXPANDED)]
178 pub display_expanded: &'static str,
179 #[prop_or(JUSTIFY_CONTENT)]
181 pub justify_content: &'static str,
182 #[prop_or(ALIGN_ITEMS)]
184 pub align_items: &'static str,
185 #[prop_or(HEIGHT)]
187 pub height: &'static str,
188
189 #[prop_or(BACKGROUND_COLOR)]
192 pub background_color: &'static str,
193 #[prop_or(FONT)]
195 pub font: &'static str,
196 #[prop_or(ICON_COLOR)]
198 pub icon_color: &'static str,
199 #[prop_or(BUTTON_BORDER_RADIUS)]
201 pub button_border_radius: &'static str,
202 #[prop_or(BUTTON_BACKGROUND_COLOR)]
204 pub button_background_color: &'static str,
205 #[prop_or(BUTTON_WIDTH)]
207 pub button_width: &'static str,
208 #[prop_or(BUTTON_HEIGHT)]
210 pub button_height: &'static str,
211
212 #[prop_or_default]
215 pub title: &'static str,
216
217 #[prop_or("images/logo.png")]
219 pub logo_src: &'static str,
220 #[prop_or("logo")]
221 pub logo_alt: &'static str,
222 #[prop_or(LOGO_IMG_CLASS)]
223 pub logo_img_class: &'static str,
224 #[prop_or("/")]
225 pub logo_link: &'static str,
226 #[prop_or(LOGO_CLASS)]
227 pub logo_class: &'static str,
228
229 #[prop_or_default]
231 pub bottom_section: Html,
232
233 #[prop_or_default]
235 pub size: &'static str,
237 #[prop_or_default]
238 pub aria_controls: &'static str,
240 #[prop_or_default]
241 pub container_class: &'static str,
243 #[prop_or_default]
244 pub expanded_element_class: &'static str,
246 #[prop_or_default]
247 pub collapsed_element_class: &'static str,
249 #[prop_or_default]
250 pub content_container_class: &'static str,
252}
253
254impl Default for SidebarProps {
255 fn default() -> Self {
256 Self {
257 fixed: false,
258 sider_collapsed: false,
259 title: "",
260 menu_items: Vec::new(),
261 width_collapsed: WIDTH_COLLAPSED,
262 width_expanded: WIDTH_EXPANDED,
263 padding_collapsed: PADDING_COLLAPSED,
264 padding_expanded: PADDING_EXPANDED,
265 display_collapsed: DISPLAY_COLLAPSED,
266 display_expanded: DISPLAY_EXPANDED,
267 justify_content: JUSTIFY_CONTENT,
268 align_items: ALIGN_ITEMS,
269 height: HEIGHT,
270 background_color: BACKGROUND_COLOR,
271 font: FONT,
272 icon_color: ICON_COLOR,
273 button_border_radius: BUTTON_BORDER_RADIUS,
274 button_background_color: BUTTON_BACKGROUND_COLOR,
275 button_width: BUTTON_WIDTH,
276 button_height: BUTTON_HEIGHT,
277 logo_src: "images/logo.png",
278 logo_alt: "logo",
279 logo_img_class: LOGO_CLASS,
280 logo_link: "/",
281 logo_class: LOGO_CLASS,
282 toggle_icon_collapsed: html! {},
283 toggle_icon_expanded: html! {},
284 bottom_section: html! {},
285 size: "md",
286 aria_controls: "accordion",
287 container_class: "text-white",
288 expanded_element_class: "text-white",
289 collapsed_element_class: "text-white",
290 content_container_class: "text-white",
291 }
292 }
293}
294
295#[function_component(Sidebar)]
296pub fn sidebar(props: &SidebarProps) -> Html {
297 let is_collapsed_handle = use_state(|| props.sider_collapsed);
298 let is_collapsed = (*is_collapsed_handle).clone();
299
300 html! {
301 <>
302 { if props.fixed {
303 html! {
304 <div
305 class={format!("transition-all duration-200 {}",
306 if is_collapsed { props.width_collapsed }
307 else { props.width_expanded })
308 }
309 />
310 }
311 } else {
312 html! {}
313 } }
314 <div
315 class={format!(
316 "{} {} {} {} {} {} {} {}",
317 if is_collapsed { props.width_collapsed } else { props.width_expanded },
318 if is_collapsed { props.padding_collapsed } else { props.padding_expanded },
319 props.display_expanded,
320 props.justify_content,
321 props.align_items,
322 props.height,
323 props.background_color,
324 props.font,
325 )}
326 >
327 { render_logo_and_title(&props, is_collapsed_handle) }
328 { render_menu(&props, is_collapsed) }
329 { props.bottom_section.clone() }
330 </div>
331 </>
332 }
333}
334
335fn render_logo_and_title(props: &SidebarProps, is_collapsed_handle: UseStateHandle<bool>) -> Html {
336 let on_toggle = {
337 let is_collapsed_handle = is_collapsed_handle.clone();
338 Callback::from(move |_| {
339 is_collapsed_handle.set(!*is_collapsed_handle);
340 })
341 };
342 let props_clone = props.clone();
343 html! {
344 <div class="flex items-center">
345 <button
346 type="button"
347 class={format!(
348 "{} {} {} {}",
349 props.button_border_radius,
350 props.button_background_color,
351 props.button_width,
352 props.button_height,
353 )}
354 onclick={on_toggle}
355 >
356 { if *is_collapsed_handle {
357 props_clone.toggle_icon_collapsed
358 } else {
359 props_clone.toggle_icon_expanded
360 } }
361 </button>
362 if !*is_collapsed_handle && !props.logo_src.is_empty() { { render_logo(&props) } }
363 if !*is_collapsed_handle { <span class="ml-2 text-white">{ props.title }</span> }
364 </div>
365 }
366}
367
368fn render_logo(props: &SidebarProps) -> Html {
369 html! {
370 <div id="logo" class={props.logo_class}>
371 <a href={props.logo_link} class="nav-link">
372 <img src={props.logo_src} alt={props.logo_alt} class={props.logo_img_class} />
373 </a>
374 </div>
375 }
376}
377
378fn render_menu(props: &SidebarProps, is_collapsed: bool) -> Html {
379 html! {
380 <ul>
381 { for props.menu_items.iter().map(|item| render_menu_item(&props, item, is_collapsed)) }
382 </ul>
383 }
384}
385
386fn render_menu_item(props: &SidebarProps, item: &MenuItem, is_collapsed: bool) -> Html {
387 let submenu_html = if !item.submenus.is_empty() {
388 html! {
389 <Accordion
390 expanded_element={html! {
391 <AccordionButton class={"text-white"}>
392 <div class="flex">
393 { item.icon.clone() }
394 if !is_collapsed { <span class="ml-2">{ &item.text }</span> }
395 </div>
396 </AccordionButton>
397 }}
398 collapsed_element={html! {
399 <AccordionButton class={"text-white"}>
400 <div class="flex">
401 { item.icon.clone() }
402 if !is_collapsed { <span class="ml-2">{ &item.text }</span> }
403 </div>
404 </AccordionButton>
405 }}
406 size={props.size}
407 aria_controls={props.aria_controls}
408 container_class={props.container_class}
409 expanded_element_class={props.expanded_element_class}
410 collapsed_element_class={props.collapsed_element_class}
411 content_container_class={props.content_container_class}
412 >
413 <ul>
414 { for item.submenus.iter().map(|submenu| render_menu_item(&props, submenu, is_collapsed)) }
415 </ul>
416 </Accordion>
417 }
418 } else {
419 html! {
420 <>{ item.icon.clone() }if !is_collapsed { <span class="ml-2">{ &item.text }</span> }</>
421 }
422 };
423 html! {
424 <li class={item.class}>
425 <div>{ item.title }</div>
426 <a href={item.link} class={MENU_ITEM}>{ submenu_html }</a>
427 </li>
428 }
429}
430
431#[derive(Clone, Properties, PartialEq)]
432pub struct MenuItem {
433 #[prop_or_default]
434 pub icon: Html,
435 #[prop_or_default]
436 pub text: &'static str,
437 #[prop_or_default]
438 pub link: &'static str,
439 #[prop_or_default]
440 pub class: &'static str,
441 #[prop_or_default]
442 pub title: &'static str,
443 #[prop_or_default]
444 pub submenus: Vec<MenuItem>,
445}