Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit e8de907

Browse files
author
FreeSynergy
committed
feat(fsn-components): add FsnSidebar collapsible sidebar component
Add FsnSidebarItem, FSN_SIDEBAR_CSS, and FsnSidebar — a 48px icon-only sidebar that expands to 220px on hover, usable by all Desktop crates.
1 parent 7bf8c00 commit e8de907

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

fsn-components/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub mod chat;
5050
// ── Re-exports ─────────────────────────────────────────────────────────────────
5151

5252
#[cfg(feature = "dioxus")]
53-
pub use nav::{SidebarNavBtn, TabBtn};
53+
pub use nav::{FsnSidebar, FsnSidebarItem, FSN_SIDEBAR_CSS, SidebarNavBtn, TabBtn};
5454
#[cfg(feature = "dioxus")]
5555
pub use button::{Button, ButtonSize, ButtonVariant, IconButton};
5656
#[cfg(feature = "dioxus")]

fsn-components/src/nav.rs

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/// Shared navigation components — tab bar buttons and sidebar nav buttons.
1+
/// Shared navigation components — tab bar buttons, sidebar nav buttons, and collapsible sidebar.
22
use dioxus::prelude::*;
33

44
/// Horizontal tab-bar button with a bottom-border active indicator.
@@ -21,6 +21,110 @@ pub fn TabBtn(label: String, is_active: bool, on_click: EventHandler) -> Element
2121
}
2222
}
2323

24+
// ── FsnSidebar ────────────────────────────────────────────────────────────────
25+
26+
/// A sidebar item descriptor.
27+
#[derive(Clone, PartialEq, Debug)]
28+
pub struct FsnSidebarItem {
29+
pub id: String,
30+
pub icon: String,
31+
pub label: String,
32+
}
33+
34+
impl FsnSidebarItem {
35+
/// Create a new sidebar item.
36+
pub fn new(id: impl Into<String>, icon: impl Into<String>, label: impl Into<String>) -> Self {
37+
Self { id: id.into(), icon: icon.into(), label: label.into() }
38+
}
39+
}
40+
41+
/// CSS for the collapsible FsnSidebar (icons-only → expands on hover).
42+
/// Inject this once at the app root.
43+
pub const FSN_SIDEBAR_CSS: &str = r#"
44+
.fsn-sidebar {
45+
width: 48px;
46+
background: var(--fsn-bg-sidebar, #0a0f1a);
47+
border-right: 1px solid var(--fsn-border, rgba(148,170,200,0.18));
48+
display: flex;
49+
flex-direction: column;
50+
overflow: hidden;
51+
transition: width 180ms ease;
52+
flex-shrink: 0;
53+
height: 100%;
54+
}
55+
.fsn-sidebar:hover { width: 220px; }
56+
.fsn-sidebar__section-label {
57+
padding: 12px 14px 4px;
58+
font-size: 10px;
59+
font-weight: 700;
60+
text-transform: uppercase;
61+
letter-spacing: 0.08em;
62+
color: var(--fsn-text-muted, #5a6e88);
63+
white-space: nowrap;
64+
overflow: hidden;
65+
opacity: 0;
66+
transition: opacity 120ms ease;
67+
}
68+
.fsn-sidebar:hover .fsn-sidebar__section-label { opacity: 1; }
69+
.fsn-sidebar__item {
70+
display: flex;
71+
align-items: center;
72+
gap: 10px;
73+
width: 100%;
74+
padding: 9px 14px;
75+
border: none;
76+
border-left: 2px solid transparent;
77+
background: transparent;
78+
cursor: pointer;
79+
color: var(--fsn-sidebar-text, #a0b0c8);
80+
white-space: nowrap;
81+
overflow: hidden;
82+
transition: background 120ms, color 120ms, border-color 120ms;
83+
text-align: left;
84+
font-size: 13px;
85+
}
86+
.fsn-sidebar__item:hover {
87+
background: var(--fsn-sidebar-hover-bg, rgba(255,255,255,0.05));
88+
color: var(--fsn-text-primary, #e8edf5);
89+
}
90+
.fsn-sidebar__item--active {
91+
background: var(--fsn-sidebar-active-bg, rgba(77,139,245,0.15));
92+
color: var(--fsn-sidebar-active, #4d8bf5);
93+
border-left-color: var(--fsn-sidebar-active, #4d8bf5);
94+
}
95+
.fsn-sidebar__icon { font-size: 18px; min-width: 20px; text-align: center; flex-shrink: 0; }
96+
.fsn-sidebar__label { font-size: 13px; overflow: hidden; text-overflow: ellipsis; }
97+
"#;
98+
99+
/// Collapsible sidebar: 48px icons-only, expands to 220px on hover.
100+
/// Inject `FSN_SIDEBAR_CSS` once at the app root.
101+
#[component]
102+
pub fn FsnSidebar(
103+
items: Vec<FsnSidebarItem>,
104+
active_id: String,
105+
on_select: EventHandler<String>,
106+
) -> Element {
107+
rsx! {
108+
nav { class: "fsn-sidebar",
109+
for item in &items {
110+
button {
111+
class: if item.id == active_id { "fsn-sidebar__item fsn-sidebar__item--active" }
112+
else { "fsn-sidebar__item" },
113+
title: "{item.label}",
114+
onclick: {
115+
let id = item.id.clone();
116+
move |_| on_select.call(id.clone())
117+
},
118+
span { class: "fsn-sidebar__icon", "{item.icon}" }
119+
span { class: "fsn-sidebar__label", "{item.label}" }
120+
}
121+
}
122+
}
123+
}
124+
}
125+
126+
// ── SidebarNavBtn ─────────────────────────────────────────────────────────────
127+
24128
/// Sidebar navigation button with icon + label and optional left-border active indicator.
25129
///
26130
/// Set `left_border = true` for conductor-style active left border;

0 commit comments

Comments
 (0)