1- /// Shared navigation components — tab bar buttons and sidebar nav buttons.
1+ /// Shared navigation components — tab bar buttons, sidebar nav buttons, and collapsible sidebar .
22use 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