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:
+2
-1
@@ -15,7 +15,8 @@
|
|||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^17.0.2"
|
"react-i18next": "^17.0.2",
|
||||||
|
"react-router-dom": "^7.14.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-a11y": "^10.3.5",
|
"@storybook/addon-a11y": "^10.3.5",
|
||||||
|
|||||||
@@ -29,7 +29,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navItem {
|
.navItem {
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: var(--space-2) var(--space-3);
|
padding: var(--space-2) var(--space-3);
|
||||||
background: none;
|
background: none;
|
||||||
@@ -40,9 +42,21 @@
|
|||||||
font-family: var(--font-family);
|
font-family: var(--font-family);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
transition: background 120ms ease, color 120ms ease;
|
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 {
|
.navItem:hover {
|
||||||
background: var(--color-bg-subtle);
|
background: var(--color-bg-subtle);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-24
@@ -4,24 +4,21 @@ import { EnvironmentsPage } from './pages/EnvironmentsPage';
|
|||||||
import { KeysPage } from './pages/KeysPage';
|
import { KeysPage } from './pages/KeysPage';
|
||||||
import { SessionsPage } from './pages/SessionsPage';
|
import { SessionsPage } from './pages/SessionsPage';
|
||||||
import { ScenariosPage } from './pages/ScenariosPage';
|
import { ScenariosPage } from './pages/ScenariosPage';
|
||||||
import { useState } from 'react';
|
import { NavLink, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
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() {
|
export default function App() {
|
||||||
const [active, setActive] = useState<Section>('scenarios');
|
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div className={styles.shell}>
|
<div className={styles.shell}>
|
||||||
<SidePanel
|
<SidePanel
|
||||||
@@ -34,25 +31,31 @@ export default function App() {
|
|||||||
width={220}
|
width={220}
|
||||||
>
|
>
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
{NAV.map(({ id, label }) => (
|
{NAV.map(({ path, labelKey, Icon }) => (
|
||||||
<button
|
<NavLink
|
||||||
key={id}
|
key={path}
|
||||||
className={`${styles.navItem} ${active === id ? styles.navItemActive : ''}`}
|
to={path}
|
||||||
onClick={() => setActive(id)}
|
className={({ isActive }) =>
|
||||||
|
[styles.navItem, isActive ? styles.navItemActive : ''].filter(Boolean).join(' ')
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{label}
|
<Icon size={16} className={styles.navIcon} aria-hidden="true" />
|
||||||
</button>
|
{t(labelKey)}
|
||||||
|
</NavLink>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</SidePanel>
|
</SidePanel>
|
||||||
|
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
{active === 'environments' && <EnvironmentsPage />}
|
<Routes>
|
||||||
{active === 'keys' && <KeysPage />}
|
<Route index element={<Navigate to="/scenarios" replace />} />
|
||||||
{active === 'sessions' && <SessionsPage />}
|
<Route path="/environments" element={<EnvironmentsPage />} />
|
||||||
{active === 'scenarios' && <ScenariosPage />}
|
<Route path="/keys" element={<KeysPage />} />
|
||||||
|
<Route path="/sessions" element={<SessionsPage />} />
|
||||||
|
<Route path="/scenarios" element={<ScenariosPage />} />
|
||||||
|
<Route path="*" element={<Navigate to="/scenarios" replace />} />
|
||||||
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -1,11 +1,14 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { HashRouter } from 'react-router-dom'
|
||||||
import './i18n'
|
import './i18n'
|
||||||
import './ui/tokens.css'
|
import './ui/tokens.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<HashRouter>
|
||||||
|
<App />
|
||||||
|
</HashRouter>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
Generated
+59
-1
@@ -20,7 +20,8 @@
|
|||||||
"lucide-react": "^1.7.0",
|
"lucide-react": "^1.7.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-i18next": "^17.0.2"
|
"react-i18next": "^17.0.2",
|
||||||
|
"react-router-dom": "^7.14.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@storybook/addon-a11y": "^10.3.5",
|
"@storybook/addon-a11y": "^10.3.5",
|
||||||
@@ -11083,6 +11084,57 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-router": {
|
||||||
|
"version": "7.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.0.tgz",
|
||||||
|
"integrity": "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cookie": "^1.0.1",
|
||||||
|
"set-cookie-parser": "^2.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router-dom": {
|
||||||
|
"version": "7.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.0.tgz",
|
||||||
|
"integrity": "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-router": "7.14.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=18",
|
||||||
|
"react-dom": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-router/node_modules/cookie": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||||
@@ -11501,6 +11553,12 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/set-cookie-parser": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/set-function-length": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user