diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index f3c243f..1f9d06d 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -84,9 +84,46 @@ pub struct SidebarShellProps { /// ``` #[component(SidebarShell)] fn sidebar_shell(props: &SidebarShellProps) -> Html { + let route = use_route::(); + let mobile_sidebar_open = use_state(|| false); + + // Close mobile sidebar automatically on any route transition + { + let mobile_sidebar_open = mobile_sidebar_open.clone(); + use_effect_with(route, move |_| { + mobile_sidebar_open.set(false); + || () + }); + } + + let on_open = { + let mobile_sidebar_open = mobile_sidebar_open.clone(); + Callback::from(move |_: MouseEvent| mobile_sidebar_open.set(true)) + }; + + let on_close = { + let mobile_sidebar_open = mobile_sidebar_open.clone(); + Callback::from(move |_: ()| mobile_sidebar_open.set(false)) + }; + + let on_close_click = { + let on_close = on_close.clone(); + Callback::from(move |_: MouseEvent| on_close.emit(())) + }; + html! {
- + + +
+ +
{ for props.children.iter() }
diff --git a/frontend/src/pages/sidebar.rs b/frontend/src/pages/sidebar.rs index 187804c..902627d 100644 --- a/frontend/src/pages/sidebar.rs +++ b/frontend/src/pages/sidebar.rs @@ -337,8 +337,16 @@ pub fn users_menu() -> Html { /// # Logout Functionality /// The "Logout" button sends a GET request to `/api/logout`, clears the user's session, /// and then redirects the user to the login page (`crate::Route::Login`). +#[derive(Properties, PartialEq)] +pub struct SidebarComponentProps { + #[prop_or_default] + pub is_open: bool, + #[prop_or_default] + pub on_close: Callback<()>, +} + #[component(Sidebar)] -pub fn sidebar() -> Html { +pub fn sidebar(props: &SidebarComponentProps) -> Html { let is_admin = use_state(|| None::); let navigator = use_navigator().expect("Sidebar must be used within a Router"); @@ -382,56 +390,78 @@ pub fn sidebar() -> Html { None => html! { }, // Non-admin: render a condensed user sidebar (no diagnostics, limited links) - Some(false) => html! { - - - + Some(false) => { + let on_close = props.on_close.clone(); + html! { + + + + } }, // Admin: full sidebar wrapped in provider so submenu state persists - Some(true) => html! { - - - + Some(true) => { + let on_close = props.on_close.clone(); + html! { + + + + } }, } } diff --git a/frontend/src/styles/components/_sidebar.scss b/frontend/src/styles/components/_sidebar.scss index e3edc26..ad90dec 100644 --- a/frontend/src/styles/components/_sidebar.scss +++ b/frontend/src/styles/components/_sidebar.scss @@ -49,9 +49,50 @@ text-align: left; } - .home { + .sidebar-header { display: flex; - justify-content: center; + align-items: center; + justify-content: space-between; + width: 100%; + margin-bottom: $spacing-md; + + .home { + flex-grow: 1; + display: flex; + justify-content: center; + + @media (max-width: 768px) { + padding-left: 36px; + } + } + + .sidebar-close { + display: none; + background: transparent; + border: none; + color: #fff; + cursor: pointer; + padding: 8px; + border-radius: 50%; + align-items: center; + justify-content: center; + transition: background 0.2s ease-in-out; + margin-bottom: 0; + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + svg { + width: 20px; + height: 20px; + stroke: #fff; + } + + @media (max-width: 768px) { + display: flex; + } + } } .home-svg { @@ -64,3 +105,70 @@ &.user { width: 220px; background: darken($color-sidebar, 6%); } } +// Floating mobile menu toggle button +.mobile-menu-toggle { + display: none; + + @media (max-width: 768px) { + display: flex; + position: fixed; + top: 1rem; + left: 1rem; + z-index: 998; + width: 48px; + height: 48px; + border-radius: 50%; + border: 1px solid rgba(128, 128, 128, 0.2); + background-color: var(--color-container); + color: var(--color-text); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + cursor: pointer; + align-items: center; + justify-content: center; + padding: 0; + transition: all 0.2s ease-in-out; + + svg { + width: 24px; + height: 24px; + stroke: var(--color-text); + transition: stroke 0.2s ease-in-out; + } + + &:hover { + transform: translateY(-1px); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2); + background-color: var(--color-bg); + } + + &:active { + transform: translateY(0); + } + } +} + +// Overlay backdrop that dims the screen and allows dismissals on mobile +.sidebar-overlay { + display: none; + + @media (max-width: 768px) { + display: block; + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background: rgba(0, 0, 0, 0.4); + backdrop-filter: blur(4px); + z-index: 999; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease-in-out; + + &.open { + opacity: 1; + pointer-events: auto; + } + } +} +