feat(client): add routing and sidebar icons

- install react-router-dom and wrap app in HashRouter to avoid dev proxy conflicts
- replace useState-based section switching with NavLink + Routes
- add lucide icons (Globe, KeyRound, Monitor, ClipboardList) to nav items
- icon opacity transitions on hover and active state via CSS
This commit is contained in:
2026-04-09 10:09:19 +03:00
parent 876b1518da
commit 50b942801f
5 changed files with 107 additions and 28 deletions
+15 -1
View File
@@ -29,7 +29,9 @@
}
.navItem {
display: block;
display: flex;
align-items: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-2) var(--space-3);
background: none;
@@ -40,9 +42,21 @@
font-family: var(--font-family);
text-align: left;
cursor: pointer;
text-decoration: none;
transition: background 120ms ease, color 120ms ease;
}
.navIcon {
flex-shrink: 0;
opacity: 0.65;
transition: opacity 120ms ease;
}
.navItem:hover .navIcon,
.navItemActive .navIcon {
opacity: 1;
}
.navItem:hover {
background: var(--color-bg-subtle);
}
+27 -24
View File
@@ -4,24 +4,21 @@ import { EnvironmentsPage } from './pages/EnvironmentsPage';
import { KeysPage } from './pages/KeysPage';
import { SessionsPage } from './pages/SessionsPage';
import { ScenariosPage } from './pages/ScenariosPage';
import { useState } from 'react';
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Globe, KeyRound, Monitor, ClipboardList } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
type Section = 'environments' | 'keys' | 'sessions' | 'scenarios';
const NAV: { path: string; labelKey: string; Icon: LucideIcon }[] = [
{ path: '/environments', labelKey: 'nav.environments', Icon: Globe },
{ path: '/keys', labelKey: 'nav.keys', Icon: KeyRound },
{ path: '/sessions', labelKey: 'nav.sessions', Icon: Monitor },
{ path: '/scenarios', labelKey: 'nav.scenarios', Icon: ClipboardList },
];
export default function App() {
const [active, setActive] = useState<Section>('scenarios');
const { t } = useTranslation();
const NAV: { id: Section; label: string }[] = [
{ id: 'environments', label: t('nav.environments') },
{ id: 'keys', label: t('nav.keys') },
{ id: 'sessions', label: t('nav.sessions') },
{ id: 'scenarios', label: t('nav.scenarios') },
];
return (
<div className={styles.shell}>
<SidePanel
@@ -34,25 +31,31 @@ export default function App() {
width={220}
>
<nav className={styles.nav}>
{NAV.map(({ id, label }) => (
<button
key={id}
className={`${styles.navItem} ${active === id ? styles.navItemActive : ''}`}
onClick={() => setActive(id)}
{NAV.map(({ path, labelKey, Icon }) => (
<NavLink
key={path}
to={path}
className={({ isActive }) =>
[styles.navItem, isActive ? styles.navItemActive : ''].filter(Boolean).join(' ')
}
>
{label}
</button>
<Icon size={16} className={styles.navIcon} aria-hidden="true" />
{t(labelKey)}
</NavLink>
))}
</nav>
</SidePanel>
<main className={styles.main}>
{active === 'environments' && <EnvironmentsPage />}
{active === 'keys' && <KeysPage />}
{active === 'sessions' && <SessionsPage />}
{active === 'scenarios' && <ScenariosPage />}
<Routes>
<Route index element={<Navigate to="/scenarios" replace />} />
<Route path="/environments" element={<EnvironmentsPage />} />
<Route path="/keys" element={<KeysPage />} />
<Route path="/sessions" element={<SessionsPage />} />
<Route path="/scenarios" element={<ScenariosPage />} />
<Route path="*" element={<Navigate to="/scenarios" replace />} />
</Routes>
</main>
</div>
);
}
+4 -1
View File
@@ -1,11 +1,14 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom'
import './i18n'
import './ui/tokens.css'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<HashRouter>
<App />
</HashRouter>
</StrictMode>,
)