ui_storybook/components/shell/
workspace_menu.rs1use leptos::prelude::*;
6
7use crate::components::menus::{
8 MenuBadgeView, MenuFooter, MenuList, MenuRow, MenuRowKind, MenuSection, PopoverPlacement,
9 PopoverSurface,
10};
11use crate::components::primitives::{BadgeKind, IconTile, IconTileKind};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct WorkspaceView {
16 pub id: &'static str,
18 pub initials: &'static str,
20 pub name: &'static str,
22 pub plan_badge: Option<&'static str>,
24 pub member_count: u32,
26 pub color: Option<&'static str>,
28}
29
30#[component]
31pub fn WorkspaceSwitcherMenu(
32 workspaces: Vec<WorkspaceView>,
34 #[prop(into)]
37 selected_id: String,
38) -> impl IntoView {
39 let rows: Vec<_> = workspaces
42 .into_iter()
43 .map(|w| {
44 let is_selected = w.id == selected_id;
45 render_row(w, is_selected)
46 })
47 .collect();
48 view! {
49 <PopoverSurface
50 placement=PopoverPlacement::BottomLeft
51 width_px=320_u16
52 title="Workspaces".to_string()
53 description="Switch between your personal and team workspaces.".to_string()
54 footer=ToChildren::to_children(|| view! {
55 <MenuFooter>
56 <span style="font-size:11px;color:var(--text-tertiary)">"Workspaces sync across devices."</span>
57 </MenuFooter>
58 })
59 >
60 <MenuList label="Workspaces">
61 <MenuSection heading="Your workspaces".to_string()>
62 {rows}
63 </MenuSection>
64 <MenuSection heading="Actions".to_string()>
65 <MenuRow
66 kind=MenuRowKind::Action
67 leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Action>"+"</IconTile> })
68 title="New workspace".to_string()
69 subtitle="Invite teammates after creating".to_string()
70 />
71 <MenuRow
72 leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Action>"⚙"</IconTile> })
73 title="Workspace settings".to_string()
74 />
75 </MenuSection>
76 </MenuList>
77 </PopoverSurface>
78 }
79}
80
81fn render_row(w: WorkspaceView, is_selected: bool) -> impl IntoView {
82 let kind = if is_selected {
83 MenuRowKind::Selected
84 } else {
85 MenuRowKind::Default
86 };
87 let initials = w.initials;
88 let color_style = w
89 .color
90 .map(|c| format!("background:{c}"))
91 .unwrap_or_default();
92 let mut badges: Vec<MenuBadgeView> = Vec::new();
93 if let Some(plan) = w.plan_badge {
94 badges.push(MenuBadgeView {
95 label: plan,
96 kind: BadgeKind::Plan,
97 });
98 }
99 let member_count_label = leak_str(format_member_count(w.member_count));
100 view! {
101 <MenuRow
102 kind=kind
103 leading=ToChildren::to_children(move || view! {
104 <span class="icon-tile icon-tile-workspace" style=color_style.clone() aria-hidden="true">{initials}</span>
105 })
106 title=w.name.to_string()
107 subtitle=member_count_label.to_string()
108 badges=badges
109 />
110 }
111}
112
113#[must_use]
116pub fn format_member_count(n: u32) -> String {
117 if n == 1 {
118 "1 member".to_string()
119 } else {
120 format!("{n} members")
121 }
122}
123
124fn leak_str(s: String) -> &'static str {
129 Box::leak(s.into_boxed_str())
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn member_count_singular() {
138 assert_eq!(format_member_count(1), "1 member");
139 }
140
141 #[test]
142 fn member_count_plural() {
143 assert_eq!(format_member_count(2), "2 members");
144 assert_eq!(format_member_count(14), "14 members");
145 assert_eq!(format_member_count(0), "0 members");
146 }
147}