skill_web/components/
sidebar.rs

1//! Side navigation component
2
3use yew::prelude::*;
4use yew_router::prelude::*;
5
6use crate::router::Route;
7use super::icons::{AnalyticsIcon, DashboardIcon, SkillsIcon, PlayIcon, HistoryIcon, SettingsIcon, SearchIcon};
8
9/// Navigation item structure
10struct NavItem {
11    route: Route,
12    label: &'static str,
13    icon: fn(&'static str) -> Html,
14}
15
16/// Sidebar navigation component
17#[function_component(Sidebar)]
18pub fn sidebar() -> Html {
19    let route = use_route::<Route>();
20
21    let nav_items = vec![
22        NavItem {
23            route: Route::Dashboard,
24            label: "Dashboard",
25            icon: |class| html! { <DashboardIcon class={class} /> },
26        },
27        NavItem {
28            route: Route::Skills,
29            label: "Skills",
30            icon: |class| html! { <SkillsIcon class={class} /> },
31        },
32        NavItem {
33            route: Route::Run,
34            label: "Run",
35            icon: |class| html! { <PlayIcon class={class} /> },
36        },
37        NavItem {
38            route: Route::History,
39            label: "History",
40            icon: |class| html! { <HistoryIcon class={class} /> },
41        },
42        NavItem {
43            route: Route::SearchTest,
44            label: "Search Test",
45            icon: |class| html! { <SearchIcon class={class} /> },
46        },
47        NavItem {
48            route: Route::Analytics,
49            label: "Analytics",
50            icon: |class| html! { <AnalyticsIcon class={class} /> },
51        },
52        NavItem {
53            route: Route::Settings,
54            label: "Settings",
55            icon: |class| html! { <SettingsIcon class={class} /> },
56        },
57    ];
58
59    html! {
60        <aside class="fixed left-0 top-16 bottom-0 w-64 bg-white dark:bg-gray-800 border-r border-gray-200 dark:border-gray-700 overflow-y-auto z-30">
61            <nav class="p-4 space-y-1">
62                { for nav_items.iter().map(|item| {
63                    let is_active = route.as_ref().map(|r| is_route_match(r, &item.route)).unwrap_or(false);
64                    let class = if is_active { "nav-link-active" } else { "nav-link" };
65
66                    html! {
67                        <Link<Route> to={item.route.clone()} classes={class}>
68                            { (item.icon)("w-5 h-5") }
69                            <span>{ item.label }</span>
70                        </Link<Route>>
71                    }
72                }) }
73            </nav>
74
75            // Bottom section with quick actions
76            <div class="absolute bottom-0 left-0 right-0 p-4 border-t border-gray-200 dark:border-gray-700">
77                <Link<Route>
78                    to={Route::Run}
79                    classes="btn btn-primary w-full"
80                >
81                    <PlayIcon class="w-4 h-4 mr-2" />
82                    { "Run Skill" }
83                </Link<Route>>
84            </div>
85        </aside>
86    }
87}
88
89/// Check if the current route matches the nav item route
90fn is_route_match(current: &Route, target: &Route) -> bool {
91    match (current, target) {
92        (Route::Dashboard, Route::Dashboard) => true,
93        (Route::Skills, Route::Skills) => true,
94        (Route::SkillDetail { .. }, Route::Skills) => true,
95        (Route::SkillInstance { .. }, Route::Skills) => true,
96        (Route::Run, Route::Run) => true,
97        (Route::RunSkill { .. }, Route::Run) => true,
98        (Route::RunSkillTool { .. }, Route::Run) => true,
99        (Route::History, Route::History) => true,
100        (Route::HistoryDetail { .. }, Route::History) => true,
101        (Route::SearchTest, Route::SearchTest) => true,
102        (Route::Analytics, Route::Analytics) => true,
103        (Route::Settings, Route::Settings) => true,
104        _ => current == target,
105    }
106}