1use super::pressable::Destination;
3use crate::components::pressable::{Pressable, Target};
4use crate::theme::{ComponentMode, classes, merge_classes, use_component_mode, use_strings};
5use dioxus::prelude::*;
6
7#[cfg(feature = "transitions")]
8use crate::components::use_shell_size;
9#[cfg(feature = "transitions")]
10use g3_route_transitions::ROUTE_TRANSITION_PERSISTENT_CLASS;
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
15pub enum AdaptiveNavCompact {
16 #[default]
18 Bar,
19 Hidden,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
26pub enum NavItemGroup {
27 #[default]
29 Primary,
30 Secondary,
32}
33
34#[derive(Clone, Copy, PartialEq)]
35enum NavKind {
36 Bar,
37 Rail,
38 Adaptive(AdaptiveNavCompact),
39}
40
41#[component]
42fn NavContainer(
43 kind: NavKind,
44 aria_label: Option<String>,
45 mode: Option<ComponentMode>,
46 class: Option<String>,
47 children: Element,
48) -> Element {
49 let mode = use_component_mode(mode);
50 let aria_label = aria_label.unwrap_or_else(|| use_strings().primary_navigation);
51 let kind_cls = match kind {
52 NavKind::Bar => "",
53 NavKind::Rail => "g3-nav-rail",
54 NavKind::Adaptive(AdaptiveNavCompact::Bar) => "g3-nav-adaptive",
55 NavKind::Adaptive(AdaptiveNavCompact::Hidden) => "g3-nav-adaptive g3-nav-compact-hidden",
56 };
57 #[cfg(feature = "transitions")]
63 let persistent_cls = {
64 let wide = use_shell_size().is_wide();
66 let is_rail = match kind {
67 NavKind::Bar => false,
68 NavKind::Rail => true,
69 NavKind::Adaptive(_) => wide,
70 };
71 if is_rail {
72 ROUTE_TRANSITION_PERSISTENT_CLASS
73 } else {
74 ""
75 }
76 };
77 #[cfg(not(feature = "transitions"))]
78 let persistent_cls = "";
79 let cls = classes([
80 "g3-nav",
81 mode.pick("g3-nav-ios", "g3-nav-md"),
82 kind_cls,
83 persistent_cls,
84 ]);
85 rsx! {
86 nav { class: merge_classes(cls, class.as_deref()), aria_label, {children} }
87 }
88}
89
90#[component]
97pub fn AdaptiveNav(
98 compact: Option<AdaptiveNavCompact>,
100 aria_label: Option<String>,
103 mode: Option<ComponentMode>,
105 class: Option<String>,
107 children: Element,
108) -> Element {
109 rsx! {
110 NavContainer {
111 kind: NavKind::Adaptive(compact.unwrap_or_default()),
112 aria_label,
113 mode,
114 class,
115 {children}
116 }
117 }
118}
119
120#[component]
123pub fn NavBar(
124 aria_label: Option<String>,
127 mode: Option<ComponentMode>,
129 class: Option<String>,
131 children: Element,
132) -> Element {
133 rsx! {
134 NavContainer { kind: NavKind::Bar, aria_label, mode, class, {children} }
135 }
136}
137
138#[component]
141pub fn NavRail(
142 aria_label: Option<String>,
145 mode: Option<ComponentMode>,
147 class: Option<String>,
149 children: Element,
150) -> Element {
151 rsx! {
152 NavContainer { kind: NavKind::Rail, aria_label, mode, class, {children} }
153 }
154}
155
156#[component]
162pub fn NavItem(
163 label: String,
165 icon: Option<Element>,
167 #[props(default, into)]
169 to: Destination,
170 href: Option<String>,
172 selected: Option<bool>,
174 badge: Option<String>,
177 group: Option<NavItemGroup>,
180 disabled: Option<bool>,
182 onclick: Option<EventHandler<MouseEvent>>,
184 class: Option<String>,
186) -> Element {
187 let target = Target::from_props(href, to, false);
188 let group_cls = match group.unwrap_or_default() {
189 NavItemGroup::Primary => "",
190 NavItemGroup::Secondary => "g3-nav-item-secondary",
191 };
192 let badge = badge.filter(|badge| !badge.is_empty());
193 let name = match &badge {
196 Some(badge) => format!("{label}, {badge}"),
197 None => label.clone(),
198 };
199 rsx! {
200 Pressable {
201 class: merge_classes(classes(["g3-nav-item", group_cls]), class.as_deref()),
202 target,
203 disabled: disabled.unwrap_or(false),
204 onclick,
205 aria_current: selected.unwrap_or(false).then_some("page"),
206 aria_label: name,
207 span { class: "g3-nav-item-icon", aria_hidden: "true",
208 {icon}
209 if let Some(badge) = &badge {
210 span { class: "g3-nav-item-badge", "{badge}" }
211 }
212 }
213 span { class: "g3-nav-item-label", aria_hidden: "true", "{label}" }
214 }
215 }
216}
217
218#[cfg(feature = "playground")]
219#[component]
220fn NavPlaygroundDemo() -> Element {
221 use dioxus_icons::lucide::{CalendarDays, CircleUserRound, Trophy};
222 let mode = crate::use_component_mode(None);
223 let mut active = use_signal(|| 0_usize);
224 let layout = use_signal(|| 0_usize);
225 let items = rsx! {
226 NavItem {
227 label: "Games",
228 selected: active() == 0,
229 badge: "3",
230 icon: rsx! {
231 Trophy { size: 20 }
232 },
233 onclick: move |_| active.set(0),
234 }
235 NavItem {
236 label: "Tourneys",
237 selected: active() == 1,
238 icon: rsx! {
239 CalendarDays { size: 20 }
240 },
241 onclick: move |_| active.set(1),
242 }
243 NavItem {
244 label: "Account",
245 selected: active() == 2,
246 group: NavItemGroup::Secondary,
247 icon: rsx! {
248 CircleUserRound { size: 20 }
249 },
250 onclick: move |_| active.set(2),
251 }
252 };
253 rsx! {
254 crate::PlaygroundDemoFrame {
255 app: false,
256 controls: rsx! {
257 div {
258 span { "Layout" }
259 crate::SegmentGroup { value: layout,
260 crate::SegmentButton { value: 0_usize, "Adaptive" }
261 crate::SegmentButton { value: 1_usize, "Bar" }
262 crate::SegmentButton { value: 2_usize, "Rail" }
263 }
264 }
265 },
266 crate::AppWrapper { mode, class: "g3-playground-device-app",
267 crate::TabLayout {
268 crate::Header { title: "Navigation" }
269 crate::Content { footer_space: false,
270 crate::Card { title: "Content", "Page content beside persistent navigation." }
271 }
272 match layout() {
273 1 => rsx! {
274 NavBar { {items} }
275 },
276 2 => rsx! {
277 NavRail { {items} }
278 },
279 _ => rsx! {
280 AdaptiveNav { {items} }
281 },
282 }
283 }
284 }
285 }
286 }
287}
288
289crate::g3_playground! {
290 name: "Navigation",
291 description: "Bottom bars and side rails, adaptive to the shell width.",
292 components: ["TabLayout", "AdaptiveNav", "NavBar", "NavRail", "NavItem"],
293 demo: NavPlaygroundDemo,
294 source: "src/components/nav.rs",
295}