Skip to main content

ui_storybook/stories/
menus.rs

1//! Menu / popover stories (M-UI.3 / AUT-123). Concrete tray menus
2//! (workspace switcher, device pickers, on-screen options, tray
3//! record popover) compose these primitives in UI-05 / UI-08 / UI-10
4//! / UI-12.
5
6use leptos::prelude::*;
7
8use crate::components::menus::{
9    MenuBadgeView, MenuFooter, MenuList, MenuRow, MenuRowKind, MenuSection, PopoverPlacement,
10    PopoverSurface,
11};
12use crate::components::primitives::{
13    BadgeKind, Button, ButtonVariant, IconTile, IconTileKind, Kbd,
14};
15
16use super::{Story, StoryViewport, render};
17
18/// All menu-surface stories, in display order.
19#[must_use]
20pub fn stories() -> Vec<Story> {
21    vec![
22        Story {
23            id: "popover-basic",
24            category: "Menus",
25            title: "Popover — header + menu list",
26            viewport: StoryViewport::Fixed {
27                width: 320,
28                height: 280,
29            },
30            render: render_popover_basic,
31        },
32        Story {
33            id: "popover-with-footer",
34            category: "Menus",
35            title: "Popover — with footer button",
36            viewport: StoryViewport::Fixed {
37                width: 320,
38                height: 320,
39            },
40            render: render_popover_with_footer,
41        },
42        Story {
43            id: "menu-row-selected",
44            category: "Menus",
45            title: "Menu row — selected",
46            viewport: StoryViewport::Fixed {
47                width: 280,
48                height: 60,
49            },
50            render: render_menu_row_selected,
51        },
52        Story {
53            id: "menu-row-action",
54            category: "Menus",
55            title: "Menu row — action",
56            viewport: StoryViewport::Fixed {
57                width: 280,
58                height: 60,
59            },
60            render: render_menu_row_action,
61        },
62        Story {
63            id: "menu-row-device",
64            category: "Menus",
65            title: "Menu row — device with subtitle + kbd",
66            viewport: StoryViewport::Fixed {
67                width: 320,
68                height: 80,
69            },
70            render: render_menu_row_device,
71        },
72        Story {
73            id: "menu-long-labels",
74            category: "Menus",
75            title: "Menu — long labels truncate cleanly",
76            viewport: StoryViewport::Fixed {
77                width: 280,
78                height: 200,
79            },
80            render: render_menu_long_labels,
81        },
82    ]
83}
84
85fn render_popover_basic() -> String {
86    render(view! {
87        <PopoverSurface
88            placement=PopoverPlacement::BottomLeft
89            width_px=280_u16
90            title="Workspaces".to_string()
91            description="Switch between your personal and team workspaces.".to_string()
92        >
93            <MenuList label="Workspaces">
94                <MenuSection heading="Personal".to_string()>
95                    <MenuRow
96                        kind=MenuRowKind::Selected
97                        leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Workspace>"PE"</IconTile> })
98                        title="Personal".to_string()
99                    />
100                </MenuSection>
101                <MenuSection heading="Teams".to_string()>
102                    <MenuRow
103                        leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Workspace>"AC"</IconTile> })
104                        title="Acme Inc.".to_string()
105                        subtitle="3 members".to_string()
106                    />
107                    <MenuRow
108                        leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Workspace>"CD"</IconTile> })
109                        title="Client demos".to_string()
110                    />
111                </MenuSection>
112            </MenuList>
113        </PopoverSurface>
114    })
115}
116
117fn render_popover_with_footer() -> String {
118    render(view! {
119        <PopoverSurface
120            placement=PopoverPlacement::BottomLeft
121            width_px=300_u16
122            title="Choose camera".to_string()
123            footer=ToChildren::to_children(|| view! {
124                <MenuFooter>
125                    <span>"Tip: press "<Kbd keys=vec!["C"] />" to switch"</span>
126                    <Button variant=ButtonVariant::Default>"Add device"</Button>
127                </MenuFooter>
128            })
129        >
130            <MenuList label="Cameras">
131                <MenuRow
132                    kind=MenuRowKind::Selected
133                    leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Device>"📷"</IconTile> })
134                    title="FaceTime HD camera".to_string()
135                    subtitle="MacBook · 1080p".to_string()
136                />
137                <MenuRow
138                    leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Device>"📷"</IconTile> })
139                    title="iPhone 15 Pro".to_string()
140                    subtitle="Continuity Camera · 4K".to_string()
141                    badges=vec![MenuBadgeView { label: "New", kind: BadgeKind::Accent }]
142                />
143                <MenuRow
144                    kind=MenuRowKind::Disabled
145                    leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Device>"📷"</IconTile> })
146                    title="OBS Virtual Camera".to_string()
147                    subtitle="Not connected".to_string()
148                />
149            </MenuList>
150        </PopoverSurface>
151    })
152}
153
154fn render_menu_row_selected() -> String {
155    render(view! {
156        <ul class="menu-list" role="menu" aria-label="example" style="width:260px;padding:6px;background:var(--surface-popover);border:1px solid var(--line-subtle);border-radius:8px;">
157            <MenuRow
158                kind=MenuRowKind::Selected
159                leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Action>"⏵"</IconTile> })
160                title="Open editor".to_string()
161                trailing=ToChildren::to_children(|| view! { <Kbd keys=vec!["⌘", "O"] /> })
162            />
163        </ul>
164    })
165}
166
167fn render_menu_row_action() -> String {
168    render(view! {
169        <ul class="menu-list" role="menu" aria-label="example" style="width:260px;padding:6px;background:var(--surface-popover);border:1px solid var(--line-subtle);border-radius:8px;">
170            <MenuRow
171                kind=MenuRowKind::Action
172                leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Action>"+"</IconTile> })
173                title="Add workspace".to_string()
174            />
175            <MenuRow
176                kind=MenuRowKind::Danger
177                leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Action>"−"</IconTile> })
178                title="Remove workspace".to_string()
179            />
180        </ul>
181    })
182}
183
184fn render_menu_row_device() -> String {
185    render(view! {
186        <ul class="menu-list" role="menu" aria-label="example" style="width:300px;padding:6px;background:var(--surface-popover);border:1px solid var(--line-subtle);border-radius:8px;">
187            <MenuRow
188                leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Device>"🎙"</IconTile> })
189                title="AirPods Pro".to_string()
190                subtitle="Bluetooth · 86% battery".to_string()
191                badges=vec![MenuBadgeView { label: "Live", kind: BadgeKind::Live }]
192                trailing=ToChildren::to_children(|| view! { <Kbd keys=vec!["⌘", "M"] /> })
193            />
194        </ul>
195    })
196}
197
198fn render_menu_long_labels() -> String {
199    render(view! {
200        <PopoverSurface placement=PopoverPlacement::BottomLeft width_px=260_u16>
201            <MenuList label="Long labels">
202                <MenuRow
203                    leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::Device>"🎙"</IconTile> })
204                    title="A very long microphone label that overflows the row width".to_string()
205                    subtitle="Subtitle that is also extremely long and needs to truncate cleanly".to_string()
206                />
207                <MenuRow
208                    leading=ToChildren::to_children(|| view! { <IconTile kind=IconTileKind::App>"S"</IconTile> })
209                    title="App name with lots of words to test truncation".to_string()
210                />
211            </MenuList>
212        </PopoverSurface>
213    })
214}